Thu, 22 Jan 2015 13:21:57 +0100
Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6
michael@0 | 1 | /* This Source Code Form is subject to the terms of the Mozilla Public |
michael@0 | 2 | * License, v. 2.0. If a copy of the MPL was not distributed with this file, |
michael@0 | 3 | * You can obtain one at http://mozilla.org/MPL/2.0/. */ |
michael@0 | 4 | |
michael@0 | 5 | "use strict"; |
michael@0 | 6 | |
michael@0 | 7 | this.EXPORTED_SYMBOLS = ['NetworkStatsDB']; |
michael@0 | 8 | |
michael@0 | 9 | const DEBUG = false; |
michael@0 | 10 | function debug(s) { dump("-*- NetworkStatsDB: " + s + "\n"); } |
michael@0 | 11 | |
michael@0 | 12 | const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; |
michael@0 | 13 | |
michael@0 | 14 | Cu.import("resource://gre/modules/Services.jsm"); |
michael@0 | 15 | Cu.import("resource://gre/modules/IndexedDBHelper.jsm"); |
michael@0 | 16 | Cu.importGlobalProperties(["indexedDB"]); |
michael@0 | 17 | |
michael@0 | 18 | const DB_NAME = "net_stats"; |
michael@0 | 19 | const DB_VERSION = 8; |
michael@0 | 20 | const DEPRECATED_STORE_NAME = "net_stats"; |
michael@0 | 21 | const STATS_STORE_NAME = "net_stats_store"; |
michael@0 | 22 | const ALARMS_STORE_NAME = "net_alarm"; |
michael@0 | 23 | |
michael@0 | 24 | // Constant defining the maximum values allowed per interface. If more, older |
michael@0 | 25 | // will be erased. |
michael@0 | 26 | const VALUES_MAX_LENGTH = 6 * 30; |
michael@0 | 27 | |
michael@0 | 28 | // Constant defining the rate of the samples. Daily. |
michael@0 | 29 | const SAMPLE_RATE = 1000 * 60 * 60 * 24; |
michael@0 | 30 | |
michael@0 | 31 | this.NetworkStatsDB = function NetworkStatsDB() { |
michael@0 | 32 | if (DEBUG) { |
michael@0 | 33 | debug("Constructor"); |
michael@0 | 34 | } |
michael@0 | 35 | this.initDBHelper(DB_NAME, DB_VERSION, [STATS_STORE_NAME, ALARMS_STORE_NAME]); |
michael@0 | 36 | } |
michael@0 | 37 | |
michael@0 | 38 | NetworkStatsDB.prototype = { |
michael@0 | 39 | __proto__: IndexedDBHelper.prototype, |
michael@0 | 40 | |
michael@0 | 41 | dbNewTxn: function dbNewTxn(store_name, txn_type, callback, txnCb) { |
michael@0 | 42 | function successCb(result) { |
michael@0 | 43 | txnCb(null, result); |
michael@0 | 44 | } |
michael@0 | 45 | function errorCb(error) { |
michael@0 | 46 | txnCb(error, null); |
michael@0 | 47 | } |
michael@0 | 48 | return this.newTxn(txn_type, store_name, callback, successCb, errorCb); |
michael@0 | 49 | }, |
michael@0 | 50 | |
michael@0 | 51 | upgradeSchema: function upgradeSchema(aTransaction, aDb, aOldVersion, aNewVersion) { |
michael@0 | 52 | if (DEBUG) { |
michael@0 | 53 | debug("upgrade schema from: " + aOldVersion + " to " + aNewVersion + " called!"); |
michael@0 | 54 | } |
michael@0 | 55 | let db = aDb; |
michael@0 | 56 | let objectStore; |
michael@0 | 57 | for (let currVersion = aOldVersion; currVersion < aNewVersion; currVersion++) { |
michael@0 | 58 | if (currVersion == 0) { |
michael@0 | 59 | /** |
michael@0 | 60 | * Create the initial database schema. |
michael@0 | 61 | */ |
michael@0 | 62 | |
michael@0 | 63 | objectStore = db.createObjectStore(DEPRECATED_STORE_NAME, { keyPath: ["connectionType", "timestamp"] }); |
michael@0 | 64 | objectStore.createIndex("connectionType", "connectionType", { unique: false }); |
michael@0 | 65 | objectStore.createIndex("timestamp", "timestamp", { unique: false }); |
michael@0 | 66 | objectStore.createIndex("rxBytes", "rxBytes", { unique: false }); |
michael@0 | 67 | objectStore.createIndex("txBytes", "txBytes", { unique: false }); |
michael@0 | 68 | objectStore.createIndex("rxTotalBytes", "rxTotalBytes", { unique: false }); |
michael@0 | 69 | objectStore.createIndex("txTotalBytes", "txTotalBytes", { unique: false }); |
michael@0 | 70 | if (DEBUG) { |
michael@0 | 71 | debug("Created object stores and indexes"); |
michael@0 | 72 | } |
michael@0 | 73 | } else if (currVersion == 2) { |
michael@0 | 74 | // In order to support per-app traffic data storage, the original |
michael@0 | 75 | // objectStore needs to be replaced by a new objectStore with new |
michael@0 | 76 | // key path ("appId") and new index ("appId"). |
michael@0 | 77 | // Also, since now networks are identified by their |
michael@0 | 78 | // [networkId, networkType] not just by their connectionType, |
michael@0 | 79 | // to modify the keyPath is mandatory to delete the object store |
michael@0 | 80 | // and create it again. Old data is going to be deleted because the |
michael@0 | 81 | // networkId for each sample can not be set. |
michael@0 | 82 | |
michael@0 | 83 | // In version 1.2 objectStore name was 'net_stats_v2', to avoid errors when |
michael@0 | 84 | // upgrading from 1.2 to 1.3 objectStore name should be checked. |
michael@0 | 85 | let stores = db.objectStoreNames; |
michael@0 | 86 | if(stores.contains("net_stats_v2")) { |
michael@0 | 87 | db.deleteObjectStore("net_stats_v2"); |
michael@0 | 88 | } else { |
michael@0 | 89 | db.deleteObjectStore(DEPRECATED_STORE_NAME); |
michael@0 | 90 | } |
michael@0 | 91 | |
michael@0 | 92 | objectStore = db.createObjectStore(DEPRECATED_STORE_NAME, { keyPath: ["appId", "network", "timestamp"] }); |
michael@0 | 93 | objectStore.createIndex("appId", "appId", { unique: false }); |
michael@0 | 94 | objectStore.createIndex("network", "network", { unique: false }); |
michael@0 | 95 | objectStore.createIndex("networkType", "networkType", { unique: false }); |
michael@0 | 96 | objectStore.createIndex("timestamp", "timestamp", { unique: false }); |
michael@0 | 97 | objectStore.createIndex("rxBytes", "rxBytes", { unique: false }); |
michael@0 | 98 | objectStore.createIndex("txBytes", "txBytes", { unique: false }); |
michael@0 | 99 | objectStore.createIndex("rxTotalBytes", "rxTotalBytes", { unique: false }); |
michael@0 | 100 | objectStore.createIndex("txTotalBytes", "txTotalBytes", { unique: false }); |
michael@0 | 101 | |
michael@0 | 102 | if (DEBUG) { |
michael@0 | 103 | debug("Created object stores and indexes for version 3"); |
michael@0 | 104 | } |
michael@0 | 105 | } else if (currVersion == 3) { |
michael@0 | 106 | // Delete redundent indexes (leave "network" only). |
michael@0 | 107 | objectStore = aTransaction.objectStore(DEPRECATED_STORE_NAME); |
michael@0 | 108 | if (objectStore.indexNames.contains("appId")) { |
michael@0 | 109 | objectStore.deleteIndex("appId"); |
michael@0 | 110 | } |
michael@0 | 111 | if (objectStore.indexNames.contains("networkType")) { |
michael@0 | 112 | objectStore.deleteIndex("networkType"); |
michael@0 | 113 | } |
michael@0 | 114 | if (objectStore.indexNames.contains("timestamp")) { |
michael@0 | 115 | objectStore.deleteIndex("timestamp"); |
michael@0 | 116 | } |
michael@0 | 117 | if (objectStore.indexNames.contains("rxBytes")) { |
michael@0 | 118 | objectStore.deleteIndex("rxBytes"); |
michael@0 | 119 | } |
michael@0 | 120 | if (objectStore.indexNames.contains("txBytes")) { |
michael@0 | 121 | objectStore.deleteIndex("txBytes"); |
michael@0 | 122 | } |
michael@0 | 123 | if (objectStore.indexNames.contains("rxTotalBytes")) { |
michael@0 | 124 | objectStore.deleteIndex("rxTotalBytes"); |
michael@0 | 125 | } |
michael@0 | 126 | if (objectStore.indexNames.contains("txTotalBytes")) { |
michael@0 | 127 | objectStore.deleteIndex("txTotalBytes"); |
michael@0 | 128 | } |
michael@0 | 129 | |
michael@0 | 130 | if (DEBUG) { |
michael@0 | 131 | debug("Deleted redundent indexes for version 4"); |
michael@0 | 132 | } |
michael@0 | 133 | } else if (currVersion == 4) { |
michael@0 | 134 | // In order to manage alarms, it is necessary to use a global counter |
michael@0 | 135 | // (totalBytes) that will increase regardless of the system reboot. |
michael@0 | 136 | objectStore = aTransaction.objectStore(DEPRECATED_STORE_NAME); |
michael@0 | 137 | |
michael@0 | 138 | // Now, systemBytes will hold the old totalBytes and totalBytes will |
michael@0 | 139 | // keep the increasing counter. |counters| will keep the track of |
michael@0 | 140 | // accumulated values. |
michael@0 | 141 | let counters = {}; |
michael@0 | 142 | |
michael@0 | 143 | objectStore.openCursor().onsuccess = function(event) { |
michael@0 | 144 | let cursor = event.target.result; |
michael@0 | 145 | if (!cursor){ |
michael@0 | 146 | return; |
michael@0 | 147 | } |
michael@0 | 148 | |
michael@0 | 149 | cursor.value.rxSystemBytes = cursor.value.rxTotalBytes; |
michael@0 | 150 | cursor.value.txSystemBytes = cursor.value.txTotalBytes; |
michael@0 | 151 | |
michael@0 | 152 | if (cursor.value.appId == 0) { |
michael@0 | 153 | let netId = cursor.value.network[0] + '' + cursor.value.network[1]; |
michael@0 | 154 | if (!counters[netId]) { |
michael@0 | 155 | counters[netId] = { |
michael@0 | 156 | rxCounter: 0, |
michael@0 | 157 | txCounter: 0, |
michael@0 | 158 | lastRx: 0, |
michael@0 | 159 | lastTx: 0 |
michael@0 | 160 | }; |
michael@0 | 161 | } |
michael@0 | 162 | |
michael@0 | 163 | let rxDiff = cursor.value.rxSystemBytes - counters[netId].lastRx; |
michael@0 | 164 | let txDiff = cursor.value.txSystemBytes - counters[netId].lastTx; |
michael@0 | 165 | if (rxDiff < 0 || txDiff < 0) { |
michael@0 | 166 | // System reboot between samples, so take the current one. |
michael@0 | 167 | rxDiff = cursor.value.rxSystemBytes; |
michael@0 | 168 | txDiff = cursor.value.txSystemBytes; |
michael@0 | 169 | } |
michael@0 | 170 | |
michael@0 | 171 | counters[netId].rxCounter += rxDiff; |
michael@0 | 172 | counters[netId].txCounter += txDiff; |
michael@0 | 173 | cursor.value.rxTotalBytes = counters[netId].rxCounter; |
michael@0 | 174 | cursor.value.txTotalBytes = counters[netId].txCounter; |
michael@0 | 175 | |
michael@0 | 176 | counters[netId].lastRx = cursor.value.rxSystemBytes; |
michael@0 | 177 | counters[netId].lastTx = cursor.value.txSystemBytes; |
michael@0 | 178 | } else { |
michael@0 | 179 | cursor.value.rxTotalBytes = cursor.value.rxSystemBytes; |
michael@0 | 180 | cursor.value.txTotalBytes = cursor.value.txSystemBytes; |
michael@0 | 181 | } |
michael@0 | 182 | |
michael@0 | 183 | cursor.update(cursor.value); |
michael@0 | 184 | cursor.continue(); |
michael@0 | 185 | }; |
michael@0 | 186 | |
michael@0 | 187 | // Create object store for alarms. |
michael@0 | 188 | objectStore = db.createObjectStore(ALARMS_STORE_NAME, { keyPath: "id", autoIncrement: true }); |
michael@0 | 189 | objectStore.createIndex("alarm", ['networkId','threshold'], { unique: false }); |
michael@0 | 190 | objectStore.createIndex("manifestURL", "manifestURL", { unique: false }); |
michael@0 | 191 | |
michael@0 | 192 | if (DEBUG) { |
michael@0 | 193 | debug("Created alarms store for version 5"); |
michael@0 | 194 | } |
michael@0 | 195 | } else if (currVersion == 5) { |
michael@0 | 196 | // In contrast to "per-app" traffic data, "system-only" traffic data |
michael@0 | 197 | // refers to data which can not be identified by any applications. |
michael@0 | 198 | // To further support "system-only" data storage, the data can be |
michael@0 | 199 | // saved by service type (e.g., Tethering, OTA). Thus it's needed to |
michael@0 | 200 | // have a new key ("serviceType") for the ojectStore. |
michael@0 | 201 | let newObjectStore; |
michael@0 | 202 | newObjectStore = db.createObjectStore(STATS_STORE_NAME, |
michael@0 | 203 | { keyPath: ["appId", "serviceType", "network", "timestamp"] }); |
michael@0 | 204 | newObjectStore.createIndex("network", "network", { unique: false }); |
michael@0 | 205 | |
michael@0 | 206 | // Copy the data from the original objectStore to the new objectStore. |
michael@0 | 207 | objectStore = aTransaction.objectStore(DEPRECATED_STORE_NAME); |
michael@0 | 208 | objectStore.openCursor().onsuccess = function(event) { |
michael@0 | 209 | let cursor = event.target.result; |
michael@0 | 210 | if (!cursor) { |
michael@0 | 211 | db.deleteObjectStore(DEPRECATED_STORE_NAME); |
michael@0 | 212 | return; |
michael@0 | 213 | } |
michael@0 | 214 | |
michael@0 | 215 | let newStats = cursor.value; |
michael@0 | 216 | newStats.serviceType = ""; |
michael@0 | 217 | newObjectStore.put(newStats); |
michael@0 | 218 | cursor.continue(); |
michael@0 | 219 | }; |
michael@0 | 220 | |
michael@0 | 221 | if (DEBUG) { |
michael@0 | 222 | debug("Added new key 'serviceType' for version 6"); |
michael@0 | 223 | } |
michael@0 | 224 | } else if (currVersion == 6) { |
michael@0 | 225 | // Replace threshold attribute of alarm index by relativeThreshold in alarms DB. |
michael@0 | 226 | // Now alarms are indexed by relativeThreshold, which is the threshold relative |
michael@0 | 227 | // to current system stats. |
michael@0 | 228 | let alarmsStore = aTransaction.objectStore(ALARMS_STORE_NAME); |
michael@0 | 229 | |
michael@0 | 230 | // Delete "alarm" index. |
michael@0 | 231 | if (alarmsStore.indexNames.contains("alarm")) { |
michael@0 | 232 | alarmsStore.deleteIndex("alarm"); |
michael@0 | 233 | } |
michael@0 | 234 | |
michael@0 | 235 | // Create new "alarm" index. |
michael@0 | 236 | alarmsStore.createIndex("alarm", ['networkId','relativeThreshold'], { unique: false }); |
michael@0 | 237 | |
michael@0 | 238 | // Populate new "alarm" index attributes. |
michael@0 | 239 | alarmsStore.openCursor().onsuccess = function(event) { |
michael@0 | 240 | let cursor = event.target.result; |
michael@0 | 241 | if (!cursor) { |
michael@0 | 242 | return; |
michael@0 | 243 | } |
michael@0 | 244 | |
michael@0 | 245 | cursor.value.relativeThreshold = cursor.value.threshold; |
michael@0 | 246 | cursor.value.absoluteThreshold = cursor.value.threshold; |
michael@0 | 247 | delete cursor.value.threshold; |
michael@0 | 248 | |
michael@0 | 249 | cursor.update(cursor.value); |
michael@0 | 250 | cursor.continue(); |
michael@0 | 251 | } |
michael@0 | 252 | |
michael@0 | 253 | // Previous versions save accumulative totalBytes, increasing althought the system |
michael@0 | 254 | // reboots or resets stats. But is necessary to reset the total counters when reset |
michael@0 | 255 | // through 'clearInterfaceStats'. |
michael@0 | 256 | let statsStore = aTransaction.objectStore(STATS_STORE_NAME); |
michael@0 | 257 | let networks = []; |
michael@0 | 258 | // Find networks stored in the database. |
michael@0 | 259 | statsStore.index("network").openKeyCursor(null, "nextunique").onsuccess = function(event) { |
michael@0 | 260 | let cursor = event.target.result; |
michael@0 | 261 | if (cursor) { |
michael@0 | 262 | networks.push(cursor.key); |
michael@0 | 263 | cursor.continue(); |
michael@0 | 264 | return; |
michael@0 | 265 | } |
michael@0 | 266 | |
michael@0 | 267 | networks.forEach(function(network) { |
michael@0 | 268 | let lowerFilter = [0, "", network, 0]; |
michael@0 | 269 | let upperFilter = [0, "", network, ""]; |
michael@0 | 270 | let range = IDBKeyRange.bound(lowerFilter, upperFilter, false, false); |
michael@0 | 271 | |
michael@0 | 272 | // Find number of samples for a given network. |
michael@0 | 273 | statsStore.count(range).onsuccess = function(event) { |
michael@0 | 274 | // If there are more samples than the max allowed, there is no way to know |
michael@0 | 275 | // when does reset take place. |
michael@0 | 276 | if (event.target.result >= VALUES_MAX_LENGTH) { |
michael@0 | 277 | return; |
michael@0 | 278 | } |
michael@0 | 279 | |
michael@0 | 280 | let last = null; |
michael@0 | 281 | // Reset detected if the first sample totalCounters are different than bytes |
michael@0 | 282 | // counters. If so, the total counters should be recalculated. |
michael@0 | 283 | statsStore.openCursor(range).onsuccess = function(event) { |
michael@0 | 284 | let cursor = event.target.result; |
michael@0 | 285 | if (!cursor) { |
michael@0 | 286 | return; |
michael@0 | 287 | } |
michael@0 | 288 | if (!last) { |
michael@0 | 289 | if (cursor.value.rxTotalBytes == cursor.value.rxBytes && |
michael@0 | 290 | cursor.value.txTotalBytes == cursor.value.txBytes) { |
michael@0 | 291 | return; |
michael@0 | 292 | } |
michael@0 | 293 | |
michael@0 | 294 | cursor.value.rxTotalBytes = cursor.value.rxBytes; |
michael@0 | 295 | cursor.value.txTotalBytes = cursor.value.txBytes; |
michael@0 | 296 | cursor.update(cursor.value); |
michael@0 | 297 | last = cursor.value; |
michael@0 | 298 | cursor.continue(); |
michael@0 | 299 | return; |
michael@0 | 300 | } |
michael@0 | 301 | |
michael@0 | 302 | // Recalculate the total counter for last / current sample |
michael@0 | 303 | cursor.value.rxTotalBytes = last.rxTotalBytes + cursor.value.rxBytes; |
michael@0 | 304 | cursor.value.txTotalBytes = last.txTotalBytes + cursor.value.txBytes; |
michael@0 | 305 | cursor.update(cursor.value); |
michael@0 | 306 | last = cursor.value; |
michael@0 | 307 | cursor.continue(); |
michael@0 | 308 | } |
michael@0 | 309 | } |
michael@0 | 310 | }, this); |
michael@0 | 311 | }; |
michael@0 | 312 | } else if (currVersion == 7) { |
michael@0 | 313 | // Create index for 'ServiceType' in order to make it retrievable. |
michael@0 | 314 | let statsStore = aTransaction.objectStore(STATS_STORE_NAME); |
michael@0 | 315 | statsStore.createIndex("serviceType", "serviceType", { unique: false }); |
michael@0 | 316 | |
michael@0 | 317 | if (DEBUG) { |
michael@0 | 318 | debug("Create index of 'serviceType' for version 8"); |
michael@0 | 319 | } |
michael@0 | 320 | } |
michael@0 | 321 | } |
michael@0 | 322 | }, |
michael@0 | 323 | |
michael@0 | 324 | importData: function importData(aStats) { |
michael@0 | 325 | let stats = { appId: aStats.appId, |
michael@0 | 326 | serviceType: aStats.serviceType, |
michael@0 | 327 | network: [aStats.networkId, aStats.networkType], |
michael@0 | 328 | timestamp: aStats.timestamp, |
michael@0 | 329 | rxBytes: aStats.rxBytes, |
michael@0 | 330 | txBytes: aStats.txBytes, |
michael@0 | 331 | rxSystemBytes: aStats.rxSystemBytes, |
michael@0 | 332 | txSystemBytes: aStats.txSystemBytes, |
michael@0 | 333 | rxTotalBytes: aStats.rxTotalBytes, |
michael@0 | 334 | txTotalBytes: aStats.txTotalBytes }; |
michael@0 | 335 | |
michael@0 | 336 | return stats; |
michael@0 | 337 | }, |
michael@0 | 338 | |
michael@0 | 339 | exportData: function exportData(aStats) { |
michael@0 | 340 | let stats = { appId: aStats.appId, |
michael@0 | 341 | serviceType: aStats.serviceType, |
michael@0 | 342 | networkId: aStats.network[0], |
michael@0 | 343 | networkType: aStats.network[1], |
michael@0 | 344 | timestamp: aStats.timestamp, |
michael@0 | 345 | rxBytes: aStats.rxBytes, |
michael@0 | 346 | txBytes: aStats.txBytes, |
michael@0 | 347 | rxTotalBytes: aStats.rxTotalBytes, |
michael@0 | 348 | txTotalBytes: aStats.txTotalBytes }; |
michael@0 | 349 | |
michael@0 | 350 | return stats; |
michael@0 | 351 | }, |
michael@0 | 352 | |
michael@0 | 353 | normalizeDate: function normalizeDate(aDate) { |
michael@0 | 354 | // Convert to UTC according to timezone and |
michael@0 | 355 | // filter timestamp to get SAMPLE_RATE precission |
michael@0 | 356 | let timestamp = aDate.getTime() - aDate.getTimezoneOffset() * 60 * 1000; |
michael@0 | 357 | timestamp = Math.floor(timestamp / SAMPLE_RATE) * SAMPLE_RATE; |
michael@0 | 358 | return timestamp; |
michael@0 | 359 | }, |
michael@0 | 360 | |
michael@0 | 361 | saveStats: function saveStats(aStats, aResultCb) { |
michael@0 | 362 | let isAccumulative = aStats.isAccumulative; |
michael@0 | 363 | let timestamp = this.normalizeDate(aStats.date); |
michael@0 | 364 | |
michael@0 | 365 | let stats = { appId: aStats.appId, |
michael@0 | 366 | serviceType: aStats.serviceType, |
michael@0 | 367 | networkId: aStats.networkId, |
michael@0 | 368 | networkType: aStats.networkType, |
michael@0 | 369 | timestamp: timestamp, |
michael@0 | 370 | rxBytes: (isAccumulative) ? 0 : aStats.rxBytes, |
michael@0 | 371 | txBytes: (isAccumulative) ? 0 : aStats.txBytes, |
michael@0 | 372 | rxSystemBytes: (isAccumulative) ? aStats.rxBytes : 0, |
michael@0 | 373 | txSystemBytes: (isAccumulative) ? aStats.txBytes : 0, |
michael@0 | 374 | rxTotalBytes: (isAccumulative) ? aStats.rxBytes : 0, |
michael@0 | 375 | txTotalBytes: (isAccumulative) ? aStats.txBytes : 0 }; |
michael@0 | 376 | |
michael@0 | 377 | stats = this.importData(stats); |
michael@0 | 378 | |
michael@0 | 379 | this.dbNewTxn(STATS_STORE_NAME, "readwrite", function(aTxn, aStore) { |
michael@0 | 380 | if (DEBUG) { |
michael@0 | 381 | debug("Filtered time: " + new Date(timestamp)); |
michael@0 | 382 | debug("New stats: " + JSON.stringify(stats)); |
michael@0 | 383 | } |
michael@0 | 384 | |
michael@0 | 385 | let request = aStore.index("network").openCursor(stats.network, "prev"); |
michael@0 | 386 | request.onsuccess = function onsuccess(event) { |
michael@0 | 387 | let cursor = event.target.result; |
michael@0 | 388 | if (!cursor) { |
michael@0 | 389 | // Empty, so save first element. |
michael@0 | 390 | |
michael@0 | 391 | // There could be a time delay between the point when the network |
michael@0 | 392 | // interface comes up and the point when the database is initialized. |
michael@0 | 393 | // In this short interval some traffic data are generated but are not |
michael@0 | 394 | // registered by the first sample. |
michael@0 | 395 | if (isAccumulative) { |
michael@0 | 396 | stats.rxBytes = stats.rxTotalBytes; |
michael@0 | 397 | stats.txBytes = stats.txTotalBytes; |
michael@0 | 398 | } |
michael@0 | 399 | |
michael@0 | 400 | this._saveStats(aTxn, aStore, stats); |
michael@0 | 401 | return; |
michael@0 | 402 | } |
michael@0 | 403 | |
michael@0 | 404 | let value = cursor.value; |
michael@0 | 405 | if (stats.appId != value.appId || |
michael@0 | 406 | (stats.appId == 0 && stats.serviceType != value.serviceType)) { |
michael@0 | 407 | cursor.continue(); |
michael@0 | 408 | return; |
michael@0 | 409 | } |
michael@0 | 410 | |
michael@0 | 411 | // There are old samples |
michael@0 | 412 | if (DEBUG) { |
michael@0 | 413 | debug("Last value " + JSON.stringify(value)); |
michael@0 | 414 | } |
michael@0 | 415 | |
michael@0 | 416 | // Remove stats previous to now - VALUE_MAX_LENGTH |
michael@0 | 417 | this._removeOldStats(aTxn, aStore, stats.appId, stats.serviceType, |
michael@0 | 418 | stats.network, stats.timestamp); |
michael@0 | 419 | |
michael@0 | 420 | // Process stats before save |
michael@0 | 421 | this._processSamplesDiff(aTxn, aStore, cursor, stats, isAccumulative); |
michael@0 | 422 | }.bind(this); |
michael@0 | 423 | }.bind(this), aResultCb); |
michael@0 | 424 | }, |
michael@0 | 425 | |
michael@0 | 426 | /* |
michael@0 | 427 | * This function check that stats are saved in the database following the sample rate. |
michael@0 | 428 | * In this way is easier to find elements when stats are requested. |
michael@0 | 429 | */ |
michael@0 | 430 | _processSamplesDiff: function _processSamplesDiff(aTxn, |
michael@0 | 431 | aStore, |
michael@0 | 432 | aLastSampleCursor, |
michael@0 | 433 | aNewSample, |
michael@0 | 434 | aIsAccumulative) { |
michael@0 | 435 | let lastSample = aLastSampleCursor.value; |
michael@0 | 436 | |
michael@0 | 437 | // Get difference between last and new sample. |
michael@0 | 438 | let diff = (aNewSample.timestamp - lastSample.timestamp) / SAMPLE_RATE; |
michael@0 | 439 | if (diff % 1) { |
michael@0 | 440 | // diff is decimal, so some error happened because samples are stored as a multiple |
michael@0 | 441 | // of SAMPLE_RATE |
michael@0 | 442 | aTxn.abort(); |
michael@0 | 443 | throw new Error("Error processing samples"); |
michael@0 | 444 | } |
michael@0 | 445 | |
michael@0 | 446 | if (DEBUG) { |
michael@0 | 447 | debug("New: " + aNewSample.timestamp + " - Last: " + |
michael@0 | 448 | lastSample.timestamp + " - diff: " + diff); |
michael@0 | 449 | } |
michael@0 | 450 | |
michael@0 | 451 | // If the incoming data has a accumulation feature, the new |
michael@0 | 452 | // |txBytes|/|rxBytes| is assigend by differnces between the new |
michael@0 | 453 | // |txTotalBytes|/|rxTotalBytes| and the last |txTotalBytes|/|rxTotalBytes|. |
michael@0 | 454 | // Else, if incoming data is non-accumulative, the |txBytes|/|rxBytes| |
michael@0 | 455 | // is the new |txBytes|/|rxBytes|. |
michael@0 | 456 | let rxDiff = 0; |
michael@0 | 457 | let txDiff = 0; |
michael@0 | 458 | if (aIsAccumulative) { |
michael@0 | 459 | rxDiff = aNewSample.rxSystemBytes - lastSample.rxSystemBytes; |
michael@0 | 460 | txDiff = aNewSample.txSystemBytes - lastSample.txSystemBytes; |
michael@0 | 461 | if (rxDiff < 0 || txDiff < 0) { |
michael@0 | 462 | rxDiff = aNewSample.rxSystemBytes; |
michael@0 | 463 | txDiff = aNewSample.txSystemBytes; |
michael@0 | 464 | } |
michael@0 | 465 | aNewSample.rxBytes = rxDiff; |
michael@0 | 466 | aNewSample.txBytes = txDiff; |
michael@0 | 467 | |
michael@0 | 468 | aNewSample.rxTotalBytes = lastSample.rxTotalBytes + rxDiff; |
michael@0 | 469 | aNewSample.txTotalBytes = lastSample.txTotalBytes + txDiff; |
michael@0 | 470 | } else { |
michael@0 | 471 | rxDiff = aNewSample.rxBytes; |
michael@0 | 472 | txDiff = aNewSample.txBytes; |
michael@0 | 473 | } |
michael@0 | 474 | |
michael@0 | 475 | if (diff == 1) { |
michael@0 | 476 | // New element. |
michael@0 | 477 | |
michael@0 | 478 | // If the incoming data is non-accumulative, the new |
michael@0 | 479 | // |rxTotalBytes|/|txTotalBytes| needs to be updated by adding new |
michael@0 | 480 | // |rxBytes|/|txBytes| to the last |rxTotalBytes|/|txTotalBytes|. |
michael@0 | 481 | if (!aIsAccumulative) { |
michael@0 | 482 | aNewSample.rxTotalBytes = aNewSample.rxBytes + lastSample.rxTotalBytes; |
michael@0 | 483 | aNewSample.txTotalBytes = aNewSample.txBytes + lastSample.txTotalBytes; |
michael@0 | 484 | } |
michael@0 | 485 | |
michael@0 | 486 | this._saveStats(aTxn, aStore, aNewSample); |
michael@0 | 487 | return; |
michael@0 | 488 | } |
michael@0 | 489 | if (diff > 1) { |
michael@0 | 490 | // Some samples lost. Device off during one or more samplerate periods. |
michael@0 | 491 | // Time or timezone changed |
michael@0 | 492 | // Add lost samples with 0 bytes and the actual one. |
michael@0 | 493 | if (diff > VALUES_MAX_LENGTH) { |
michael@0 | 494 | diff = VALUES_MAX_LENGTH; |
michael@0 | 495 | } |
michael@0 | 496 | |
michael@0 | 497 | let data = []; |
michael@0 | 498 | for (let i = diff - 2; i >= 0; i--) { |
michael@0 | 499 | let time = aNewSample.timestamp - SAMPLE_RATE * (i + 1); |
michael@0 | 500 | let sample = { appId: aNewSample.appId, |
michael@0 | 501 | serviceType: aNewSample.serviceType, |
michael@0 | 502 | network: aNewSample.network, |
michael@0 | 503 | timestamp: time, |
michael@0 | 504 | rxBytes: 0, |
michael@0 | 505 | txBytes: 0, |
michael@0 | 506 | rxSystemBytes: lastSample.rxSystemBytes, |
michael@0 | 507 | txSystemBytes: lastSample.txSystemBytes, |
michael@0 | 508 | rxTotalBytes: lastSample.rxTotalBytes, |
michael@0 | 509 | txTotalBytes: lastSample.txTotalBytes }; |
michael@0 | 510 | |
michael@0 | 511 | data.push(sample); |
michael@0 | 512 | } |
michael@0 | 513 | |
michael@0 | 514 | data.push(aNewSample); |
michael@0 | 515 | this._saveStats(aTxn, aStore, data); |
michael@0 | 516 | return; |
michael@0 | 517 | } |
michael@0 | 518 | if (diff == 0 || diff < 0) { |
michael@0 | 519 | // New element received before samplerate period. It means that device has |
michael@0 | 520 | // been restarted (or clock / timezone change). |
michael@0 | 521 | // Update element. If diff < 0, clock or timezone changed back. Place data |
michael@0 | 522 | // in the last sample. |
michael@0 | 523 | |
michael@0 | 524 | // Old |rxTotalBytes|/|txTotalBytes| needs to get updated by adding the |
michael@0 | 525 | // last |rxTotalBytes|/|txTotalBytes|. |
michael@0 | 526 | lastSample.rxBytes += rxDiff; |
michael@0 | 527 | lastSample.txBytes += txDiff; |
michael@0 | 528 | lastSample.rxSystemBytes = aNewSample.rxSystemBytes; |
michael@0 | 529 | lastSample.txSystemBytes = aNewSample.txSystemBytes; |
michael@0 | 530 | lastSample.rxTotalBytes += rxDiff; |
michael@0 | 531 | lastSample.txTotalBytes += txDiff; |
michael@0 | 532 | |
michael@0 | 533 | if (DEBUG) { |
michael@0 | 534 | debug("Update: " + JSON.stringify(lastSample)); |
michael@0 | 535 | } |
michael@0 | 536 | let req = aLastSampleCursor.update(lastSample); |
michael@0 | 537 | } |
michael@0 | 538 | }, |
michael@0 | 539 | |
michael@0 | 540 | _saveStats: function _saveStats(aTxn, aStore, aNetworkStats) { |
michael@0 | 541 | if (DEBUG) { |
michael@0 | 542 | debug("_saveStats: " + JSON.stringify(aNetworkStats)); |
michael@0 | 543 | } |
michael@0 | 544 | |
michael@0 | 545 | if (Array.isArray(aNetworkStats)) { |
michael@0 | 546 | let len = aNetworkStats.length - 1; |
michael@0 | 547 | for (let i = 0; i <= len; i++) { |
michael@0 | 548 | aStore.put(aNetworkStats[i]); |
michael@0 | 549 | } |
michael@0 | 550 | } else { |
michael@0 | 551 | aStore.put(aNetworkStats); |
michael@0 | 552 | } |
michael@0 | 553 | }, |
michael@0 | 554 | |
michael@0 | 555 | _removeOldStats: function _removeOldStats(aTxn, aStore, aAppId, aServiceType, |
michael@0 | 556 | aNetwork, aDate) { |
michael@0 | 557 | // Callback function to remove old items when new ones are added. |
michael@0 | 558 | let filterDate = aDate - (SAMPLE_RATE * VALUES_MAX_LENGTH - 1); |
michael@0 | 559 | let lowerFilter = [aAppId, aServiceType, aNetwork, 0]; |
michael@0 | 560 | let upperFilter = [aAppId, aServiceType, aNetwork, filterDate]; |
michael@0 | 561 | let range = IDBKeyRange.bound(lowerFilter, upperFilter, false, false); |
michael@0 | 562 | let lastSample = null; |
michael@0 | 563 | let self = this; |
michael@0 | 564 | |
michael@0 | 565 | aStore.openCursor(range).onsuccess = function(event) { |
michael@0 | 566 | var cursor = event.target.result; |
michael@0 | 567 | if (cursor) { |
michael@0 | 568 | lastSample = cursor.value; |
michael@0 | 569 | cursor.delete(); |
michael@0 | 570 | cursor.continue(); |
michael@0 | 571 | return; |
michael@0 | 572 | } |
michael@0 | 573 | |
michael@0 | 574 | // If all samples for a network are removed, an empty sample |
michael@0 | 575 | // has to be saved to keep the totalBytes in order to compute |
michael@0 | 576 | // future samples because system counters are not set to 0. |
michael@0 | 577 | // Thus, if there are no samples left, the last sample removed |
michael@0 | 578 | // will be saved again after setting its bytes to 0. |
michael@0 | 579 | let request = aStore.index("network").openCursor(aNetwork); |
michael@0 | 580 | request.onsuccess = function onsuccess(event) { |
michael@0 | 581 | let cursor = event.target.result; |
michael@0 | 582 | if (!cursor && lastSample != null) { |
michael@0 | 583 | let timestamp = new Date(); |
michael@0 | 584 | timestamp = self.normalizeDate(timestamp); |
michael@0 | 585 | lastSample.timestamp = timestamp; |
michael@0 | 586 | lastSample.rxBytes = 0; |
michael@0 | 587 | lastSample.txBytes = 0; |
michael@0 | 588 | self._saveStats(aTxn, aStore, lastSample); |
michael@0 | 589 | } |
michael@0 | 590 | }; |
michael@0 | 591 | }; |
michael@0 | 592 | }, |
michael@0 | 593 | |
michael@0 | 594 | clearInterfaceStats: function clearInterfaceStats(aNetwork, aResultCb) { |
michael@0 | 595 | let network = [aNetwork.network.id, aNetwork.network.type]; |
michael@0 | 596 | let self = this; |
michael@0 | 597 | |
michael@0 | 598 | // Clear and save an empty sample to keep sync with system counters |
michael@0 | 599 | this.dbNewTxn(STATS_STORE_NAME, "readwrite", function(aTxn, aStore) { |
michael@0 | 600 | let sample = null; |
michael@0 | 601 | let request = aStore.index("network").openCursor(network, "prev"); |
michael@0 | 602 | request.onsuccess = function onsuccess(event) { |
michael@0 | 603 | let cursor = event.target.result; |
michael@0 | 604 | if (cursor) { |
michael@0 | 605 | if (!sample && cursor.value.appId == 0) { |
michael@0 | 606 | sample = cursor.value; |
michael@0 | 607 | } |
michael@0 | 608 | |
michael@0 | 609 | cursor.delete(); |
michael@0 | 610 | cursor.continue(); |
michael@0 | 611 | return; |
michael@0 | 612 | } |
michael@0 | 613 | |
michael@0 | 614 | if (sample) { |
michael@0 | 615 | let timestamp = new Date(); |
michael@0 | 616 | timestamp = self.normalizeDate(timestamp); |
michael@0 | 617 | sample.timestamp = timestamp; |
michael@0 | 618 | sample.appId = 0; |
michael@0 | 619 | sample.serviceType = ""; |
michael@0 | 620 | sample.rxBytes = 0; |
michael@0 | 621 | sample.txBytes = 0; |
michael@0 | 622 | sample.rxTotalBytes = 0; |
michael@0 | 623 | sample.txTotalBytes = 0; |
michael@0 | 624 | |
michael@0 | 625 | self._saveStats(aTxn, aStore, sample); |
michael@0 | 626 | } |
michael@0 | 627 | }; |
michael@0 | 628 | }, this._resetAlarms.bind(this, aNetwork.networkId, aResultCb)); |
michael@0 | 629 | }, |
michael@0 | 630 | |
michael@0 | 631 | clearStats: function clearStats(aNetworks, aResultCb) { |
michael@0 | 632 | let index = 0; |
michael@0 | 633 | let stats = []; |
michael@0 | 634 | let self = this; |
michael@0 | 635 | |
michael@0 | 636 | let callback = function(aError, aResult) { |
michael@0 | 637 | index++; |
michael@0 | 638 | |
michael@0 | 639 | if (!aError && index < aNetworks.length) { |
michael@0 | 640 | self.clearInterfaceStats(aNetworks[index], callback); |
michael@0 | 641 | return; |
michael@0 | 642 | } |
michael@0 | 643 | |
michael@0 | 644 | aResultCb(aError, aResult); |
michael@0 | 645 | }; |
michael@0 | 646 | |
michael@0 | 647 | if (!aNetworks[index]) { |
michael@0 | 648 | aResultCb(null, true); |
michael@0 | 649 | return; |
michael@0 | 650 | } |
michael@0 | 651 | this.clearInterfaceStats(aNetworks[index], callback); |
michael@0 | 652 | }, |
michael@0 | 653 | |
michael@0 | 654 | getCurrentStats: function getCurrentStats(aNetwork, aDate, aResultCb) { |
michael@0 | 655 | if (DEBUG) { |
michael@0 | 656 | debug("Get current stats for " + JSON.stringify(aNetwork) + " since " + aDate); |
michael@0 | 657 | } |
michael@0 | 658 | |
michael@0 | 659 | let network = [aNetwork.id, aNetwork.type]; |
michael@0 | 660 | if (aDate) { |
michael@0 | 661 | this._getCurrentStatsFromDate(network, aDate, aResultCb); |
michael@0 | 662 | return; |
michael@0 | 663 | } |
michael@0 | 664 | |
michael@0 | 665 | this._getCurrentStats(network, aResultCb); |
michael@0 | 666 | }, |
michael@0 | 667 | |
michael@0 | 668 | _getCurrentStats: function _getCurrentStats(aNetwork, aResultCb) { |
michael@0 | 669 | this.dbNewTxn(STATS_STORE_NAME, "readonly", function(txn, store) { |
michael@0 | 670 | let request = null; |
michael@0 | 671 | let upperFilter = [0, "", aNetwork, Date.now()]; |
michael@0 | 672 | let range = IDBKeyRange.upperBound(upperFilter, false); |
michael@0 | 673 | request = store.openCursor(range, "prev"); |
michael@0 | 674 | |
michael@0 | 675 | let result = { rxBytes: 0, txBytes: 0, |
michael@0 | 676 | rxTotalBytes: 0, txTotalBytes: 0 }; |
michael@0 | 677 | |
michael@0 | 678 | request.onsuccess = function onsuccess(event) { |
michael@0 | 679 | let cursor = event.target.result; |
michael@0 | 680 | if (cursor) { |
michael@0 | 681 | result.rxBytes = result.rxTotalBytes = cursor.value.rxTotalBytes; |
michael@0 | 682 | result.txBytes = result.txTotalBytes = cursor.value.txTotalBytes; |
michael@0 | 683 | } |
michael@0 | 684 | |
michael@0 | 685 | txn.result = result; |
michael@0 | 686 | }; |
michael@0 | 687 | }.bind(this), aResultCb); |
michael@0 | 688 | }, |
michael@0 | 689 | |
michael@0 | 690 | _getCurrentStatsFromDate: function _getCurrentStatsFromDate(aNetwork, aDate, aResultCb) { |
michael@0 | 691 | aDate = new Date(aDate); |
michael@0 | 692 | this.dbNewTxn(STATS_STORE_NAME, "readonly", function(txn, store) { |
michael@0 | 693 | let request = null; |
michael@0 | 694 | let start = this.normalizeDate(aDate); |
michael@0 | 695 | let lowerFilter = [0, "", aNetwork, start]; |
michael@0 | 696 | let upperFilter = [0, "", aNetwork, Date.now()]; |
michael@0 | 697 | |
michael@0 | 698 | let range = IDBKeyRange.upperBound(upperFilter, false); |
michael@0 | 699 | |
michael@0 | 700 | let result = { rxBytes: 0, txBytes: 0, |
michael@0 | 701 | rxTotalBytes: 0, txTotalBytes: 0 }; |
michael@0 | 702 | |
michael@0 | 703 | request = store.openCursor(range, "prev"); |
michael@0 | 704 | |
michael@0 | 705 | request.onsuccess = function onsuccess(event) { |
michael@0 | 706 | let cursor = event.target.result; |
michael@0 | 707 | if (cursor) { |
michael@0 | 708 | result.rxBytes = result.rxTotalBytes = cursor.value.rxTotalBytes; |
michael@0 | 709 | result.txBytes = result.txTotalBytes = cursor.value.txTotalBytes; |
michael@0 | 710 | } |
michael@0 | 711 | |
michael@0 | 712 | let timestamp = cursor.value.timestamp; |
michael@0 | 713 | let range = IDBKeyRange.lowerBound(lowerFilter, false); |
michael@0 | 714 | request = store.openCursor(range); |
michael@0 | 715 | |
michael@0 | 716 | request.onsuccess = function onsuccess(event) { |
michael@0 | 717 | let cursor = event.target.result; |
michael@0 | 718 | if (cursor) { |
michael@0 | 719 | if (cursor.value.timestamp == timestamp) { |
michael@0 | 720 | // There is one sample only. |
michael@0 | 721 | result.rxBytes = cursor.value.rxBytes; |
michael@0 | 722 | result.txBytes = cursor.value.txBytes; |
michael@0 | 723 | } else { |
michael@0 | 724 | result.rxBytes -= cursor.value.rxTotalBytes; |
michael@0 | 725 | result.txBytes -= cursor.value.txTotalBytes; |
michael@0 | 726 | } |
michael@0 | 727 | } |
michael@0 | 728 | |
michael@0 | 729 | txn.result = result; |
michael@0 | 730 | }; |
michael@0 | 731 | }; |
michael@0 | 732 | }.bind(this), aResultCb); |
michael@0 | 733 | }, |
michael@0 | 734 | |
michael@0 | 735 | find: function find(aResultCb, aAppId, aServiceType, aNetwork, |
michael@0 | 736 | aStart, aEnd, aAppManifestURL) { |
michael@0 | 737 | let offset = (new Date()).getTimezoneOffset() * 60 * 1000; |
michael@0 | 738 | let start = this.normalizeDate(aStart); |
michael@0 | 739 | let end = this.normalizeDate(aEnd); |
michael@0 | 740 | |
michael@0 | 741 | if (DEBUG) { |
michael@0 | 742 | debug("Find samples for appId: " + aAppId + " serviceType: " + |
michael@0 | 743 | aServiceType + " network: " + JSON.stringify(aNetwork) + " from " + |
michael@0 | 744 | start + " until " + end); |
michael@0 | 745 | debug("Start time: " + new Date(start)); |
michael@0 | 746 | debug("End time: " + new Date(end)); |
michael@0 | 747 | } |
michael@0 | 748 | |
michael@0 | 749 | this.dbNewTxn(STATS_STORE_NAME, "readonly", function(aTxn, aStore) { |
michael@0 | 750 | let network = [aNetwork.id, aNetwork.type]; |
michael@0 | 751 | let lowerFilter = [aAppId, aServiceType, network, start]; |
michael@0 | 752 | let upperFilter = [aAppId, aServiceType, network, end]; |
michael@0 | 753 | let range = IDBKeyRange.bound(lowerFilter, upperFilter, false, false); |
michael@0 | 754 | |
michael@0 | 755 | let data = []; |
michael@0 | 756 | |
michael@0 | 757 | if (!aTxn.result) { |
michael@0 | 758 | aTxn.result = {}; |
michael@0 | 759 | } |
michael@0 | 760 | |
michael@0 | 761 | let request = aStore.openCursor(range).onsuccess = function(event) { |
michael@0 | 762 | var cursor = event.target.result; |
michael@0 | 763 | if (cursor){ |
michael@0 | 764 | data.push({ rxBytes: cursor.value.rxBytes, |
michael@0 | 765 | txBytes: cursor.value.txBytes, |
michael@0 | 766 | date: new Date(cursor.value.timestamp + offset) }); |
michael@0 | 767 | cursor.continue(); |
michael@0 | 768 | return; |
michael@0 | 769 | } |
michael@0 | 770 | |
michael@0 | 771 | // When requested samples (start / end) are not in the range of now and |
michael@0 | 772 | // now - VALUES_MAX_LENGTH, fill with empty samples. |
michael@0 | 773 | this.fillResultSamples(start + offset, end + offset, data); |
michael@0 | 774 | |
michael@0 | 775 | aTxn.result.appManifestURL = aAppManifestURL; |
michael@0 | 776 | aTxn.result.serviceType = aServiceType; |
michael@0 | 777 | aTxn.result.network = aNetwork; |
michael@0 | 778 | aTxn.result.start = aStart; |
michael@0 | 779 | aTxn.result.end = aEnd; |
michael@0 | 780 | aTxn.result.data = data; |
michael@0 | 781 | }.bind(this); |
michael@0 | 782 | }.bind(this), aResultCb); |
michael@0 | 783 | }, |
michael@0 | 784 | |
michael@0 | 785 | /* |
michael@0 | 786 | * Fill data array (samples from database) with empty samples to match |
michael@0 | 787 | * requested start / end dates. |
michael@0 | 788 | */ |
michael@0 | 789 | fillResultSamples: function fillResultSamples(aStart, aEnd, aData) { |
michael@0 | 790 | if (aData.length == 0) { |
michael@0 | 791 | aData.push({ rxBytes: undefined, |
michael@0 | 792 | txBytes: undefined, |
michael@0 | 793 | date: new Date(aStart) }); |
michael@0 | 794 | } |
michael@0 | 795 | |
michael@0 | 796 | while (aStart < aData[0].date.getTime()) { |
michael@0 | 797 | aData.unshift({ rxBytes: undefined, |
michael@0 | 798 | txBytes: undefined, |
michael@0 | 799 | date: new Date(aData[0].date.getTime() - SAMPLE_RATE) }); |
michael@0 | 800 | } |
michael@0 | 801 | |
michael@0 | 802 | while (aEnd > aData[aData.length - 1].date.getTime()) { |
michael@0 | 803 | aData.push({ rxBytes: undefined, |
michael@0 | 804 | txBytes: undefined, |
michael@0 | 805 | date: new Date(aData[aData.length - 1].date.getTime() + SAMPLE_RATE) }); |
michael@0 | 806 | } |
michael@0 | 807 | }, |
michael@0 | 808 | |
michael@0 | 809 | getAvailableNetworks: function getAvailableNetworks(aResultCb) { |
michael@0 | 810 | this.dbNewTxn(STATS_STORE_NAME, "readonly", function(aTxn, aStore) { |
michael@0 | 811 | if (!aTxn.result) { |
michael@0 | 812 | aTxn.result = []; |
michael@0 | 813 | } |
michael@0 | 814 | |
michael@0 | 815 | let request = aStore.index("network").openKeyCursor(null, "nextunique"); |
michael@0 | 816 | request.onsuccess = function onsuccess(event) { |
michael@0 | 817 | let cursor = event.target.result; |
michael@0 | 818 | if (cursor) { |
michael@0 | 819 | aTxn.result.push({ id: cursor.key[0], |
michael@0 | 820 | type: cursor.key[1] }); |
michael@0 | 821 | cursor.continue(); |
michael@0 | 822 | return; |
michael@0 | 823 | } |
michael@0 | 824 | }; |
michael@0 | 825 | }, aResultCb); |
michael@0 | 826 | }, |
michael@0 | 827 | |
michael@0 | 828 | isNetworkAvailable: function isNetworkAvailable(aNetwork, aResultCb) { |
michael@0 | 829 | this.dbNewTxn(STATS_STORE_NAME, "readonly", function(aTxn, aStore) { |
michael@0 | 830 | if (!aTxn.result) { |
michael@0 | 831 | aTxn.result = false; |
michael@0 | 832 | } |
michael@0 | 833 | |
michael@0 | 834 | let network = [aNetwork.id, aNetwork.type]; |
michael@0 | 835 | let request = aStore.index("network").openKeyCursor(IDBKeyRange.only(network)); |
michael@0 | 836 | request.onsuccess = function onsuccess(event) { |
michael@0 | 837 | if (event.target.result) { |
michael@0 | 838 | aTxn.result = true; |
michael@0 | 839 | } |
michael@0 | 840 | }; |
michael@0 | 841 | }, aResultCb); |
michael@0 | 842 | }, |
michael@0 | 843 | |
michael@0 | 844 | getAvailableServiceTypes: function getAvailableServiceTypes(aResultCb) { |
michael@0 | 845 | this.dbNewTxn(STATS_STORE_NAME, "readonly", function(aTxn, aStore) { |
michael@0 | 846 | if (!aTxn.result) { |
michael@0 | 847 | aTxn.result = []; |
michael@0 | 848 | } |
michael@0 | 849 | |
michael@0 | 850 | let request = aStore.index("serviceType").openKeyCursor(null, "nextunique"); |
michael@0 | 851 | request.onsuccess = function onsuccess(event) { |
michael@0 | 852 | let cursor = event.target.result; |
michael@0 | 853 | if (cursor && cursor.key != "") { |
michael@0 | 854 | aTxn.result.push({ serviceType: cursor.key }); |
michael@0 | 855 | cursor.continue(); |
michael@0 | 856 | return; |
michael@0 | 857 | } |
michael@0 | 858 | }; |
michael@0 | 859 | }, aResultCb); |
michael@0 | 860 | }, |
michael@0 | 861 | |
michael@0 | 862 | get sampleRate () { |
michael@0 | 863 | return SAMPLE_RATE; |
michael@0 | 864 | }, |
michael@0 | 865 | |
michael@0 | 866 | get maxStorageSamples () { |
michael@0 | 867 | return VALUES_MAX_LENGTH; |
michael@0 | 868 | }, |
michael@0 | 869 | |
michael@0 | 870 | logAllRecords: function logAllRecords(aResultCb) { |
michael@0 | 871 | this.dbNewTxn(STATS_STORE_NAME, "readonly", function(aTxn, aStore) { |
michael@0 | 872 | aStore.mozGetAll().onsuccess = function onsuccess(event) { |
michael@0 | 873 | aTxn.result = event.target.result; |
michael@0 | 874 | }; |
michael@0 | 875 | }, aResultCb); |
michael@0 | 876 | }, |
michael@0 | 877 | |
michael@0 | 878 | alarmToRecord: function alarmToRecord(aAlarm) { |
michael@0 | 879 | let record = { networkId: aAlarm.networkId, |
michael@0 | 880 | absoluteThreshold: aAlarm.absoluteThreshold, |
michael@0 | 881 | relativeThreshold: aAlarm.relativeThreshold, |
michael@0 | 882 | startTime: aAlarm.startTime, |
michael@0 | 883 | data: aAlarm.data, |
michael@0 | 884 | manifestURL: aAlarm.manifestURL, |
michael@0 | 885 | pageURL: aAlarm.pageURL }; |
michael@0 | 886 | |
michael@0 | 887 | if (aAlarm.id) { |
michael@0 | 888 | record.id = aAlarm.id; |
michael@0 | 889 | } |
michael@0 | 890 | |
michael@0 | 891 | return record; |
michael@0 | 892 | }, |
michael@0 | 893 | |
michael@0 | 894 | recordToAlarm: function recordToalarm(aRecord) { |
michael@0 | 895 | let alarm = { networkId: aRecord.networkId, |
michael@0 | 896 | absoluteThreshold: aRecord.absoluteThreshold, |
michael@0 | 897 | relativeThreshold: aRecord.relativeThreshold, |
michael@0 | 898 | startTime: aRecord.startTime, |
michael@0 | 899 | data: aRecord.data, |
michael@0 | 900 | manifestURL: aRecord.manifestURL, |
michael@0 | 901 | pageURL: aRecord.pageURL }; |
michael@0 | 902 | |
michael@0 | 903 | if (aRecord.id) { |
michael@0 | 904 | alarm.id = aRecord.id; |
michael@0 | 905 | } |
michael@0 | 906 | |
michael@0 | 907 | return alarm; |
michael@0 | 908 | }, |
michael@0 | 909 | |
michael@0 | 910 | addAlarm: function addAlarm(aAlarm, aResultCb) { |
michael@0 | 911 | this.dbNewTxn(ALARMS_STORE_NAME, "readwrite", function(txn, store) { |
michael@0 | 912 | if (DEBUG) { |
michael@0 | 913 | debug("Going to add " + JSON.stringify(aAlarm)); |
michael@0 | 914 | } |
michael@0 | 915 | |
michael@0 | 916 | let record = this.alarmToRecord(aAlarm); |
michael@0 | 917 | store.put(record).onsuccess = function setResult(aEvent) { |
michael@0 | 918 | txn.result = aEvent.target.result; |
michael@0 | 919 | if (DEBUG) { |
michael@0 | 920 | debug("Request successful. New record ID: " + txn.result); |
michael@0 | 921 | } |
michael@0 | 922 | }; |
michael@0 | 923 | }.bind(this), aResultCb); |
michael@0 | 924 | }, |
michael@0 | 925 | |
michael@0 | 926 | getFirstAlarm: function getFirstAlarm(aNetworkId, aResultCb) { |
michael@0 | 927 | let self = this; |
michael@0 | 928 | |
michael@0 | 929 | this.dbNewTxn(ALARMS_STORE_NAME, "readonly", function(txn, store) { |
michael@0 | 930 | if (DEBUG) { |
michael@0 | 931 | debug("Get first alarm for network " + aNetworkId); |
michael@0 | 932 | } |
michael@0 | 933 | |
michael@0 | 934 | let lowerFilter = [aNetworkId, 0]; |
michael@0 | 935 | let upperFilter = [aNetworkId, ""]; |
michael@0 | 936 | let range = IDBKeyRange.bound(lowerFilter, upperFilter); |
michael@0 | 937 | |
michael@0 | 938 | store.index("alarm").openCursor(range).onsuccess = function onsuccess(event) { |
michael@0 | 939 | let cursor = event.target.result; |
michael@0 | 940 | txn.result = null; |
michael@0 | 941 | if (cursor) { |
michael@0 | 942 | txn.result = self.recordToAlarm(cursor.value); |
michael@0 | 943 | } |
michael@0 | 944 | }; |
michael@0 | 945 | }, aResultCb); |
michael@0 | 946 | }, |
michael@0 | 947 | |
michael@0 | 948 | removeAlarm: function removeAlarm(aAlarmId, aManifestURL, aResultCb) { |
michael@0 | 949 | this.dbNewTxn(ALARMS_STORE_NAME, "readwrite", function(txn, store) { |
michael@0 | 950 | if (DEBUG) { |
michael@0 | 951 | debug("Remove alarm " + aAlarmId); |
michael@0 | 952 | } |
michael@0 | 953 | |
michael@0 | 954 | store.get(aAlarmId).onsuccess = function onsuccess(event) { |
michael@0 | 955 | let record = event.target.result; |
michael@0 | 956 | txn.result = false; |
michael@0 | 957 | if (!record || (aManifestURL && record.manifestURL != aManifestURL)) { |
michael@0 | 958 | return; |
michael@0 | 959 | } |
michael@0 | 960 | |
michael@0 | 961 | store.delete(aAlarmId); |
michael@0 | 962 | txn.result = true; |
michael@0 | 963 | } |
michael@0 | 964 | }, aResultCb); |
michael@0 | 965 | }, |
michael@0 | 966 | |
michael@0 | 967 | removeAlarms: function removeAlarms(aManifestURL, aResultCb) { |
michael@0 | 968 | this.dbNewTxn(ALARMS_STORE_NAME, "readwrite", function(txn, store) { |
michael@0 | 969 | if (DEBUG) { |
michael@0 | 970 | debug("Remove alarms of " + aManifestURL); |
michael@0 | 971 | } |
michael@0 | 972 | |
michael@0 | 973 | store.index("manifestURL").openCursor(aManifestURL) |
michael@0 | 974 | .onsuccess = function onsuccess(event) { |
michael@0 | 975 | let cursor = event.target.result; |
michael@0 | 976 | if (cursor) { |
michael@0 | 977 | cursor.delete(); |
michael@0 | 978 | cursor.continue(); |
michael@0 | 979 | } |
michael@0 | 980 | } |
michael@0 | 981 | }, aResultCb); |
michael@0 | 982 | }, |
michael@0 | 983 | |
michael@0 | 984 | updateAlarm: function updateAlarm(aAlarm, aResultCb) { |
michael@0 | 985 | let self = this; |
michael@0 | 986 | this.dbNewTxn(ALARMS_STORE_NAME, "readwrite", function(txn, store) { |
michael@0 | 987 | if (DEBUG) { |
michael@0 | 988 | debug("Update alarm " + aAlarm.id); |
michael@0 | 989 | } |
michael@0 | 990 | |
michael@0 | 991 | let record = self.alarmToRecord(aAlarm); |
michael@0 | 992 | store.openCursor(record.id).onsuccess = function onsuccess(event) { |
michael@0 | 993 | let cursor = event.target.result; |
michael@0 | 994 | txn.result = false; |
michael@0 | 995 | if (cursor) { |
michael@0 | 996 | cursor.update(record); |
michael@0 | 997 | txn.result = true; |
michael@0 | 998 | } |
michael@0 | 999 | } |
michael@0 | 1000 | }, aResultCb); |
michael@0 | 1001 | }, |
michael@0 | 1002 | |
michael@0 | 1003 | getAlarms: function getAlarms(aNetworkId, aManifestURL, aResultCb) { |
michael@0 | 1004 | let self = this; |
michael@0 | 1005 | this.dbNewTxn(ALARMS_STORE_NAME, "readonly", function(txn, store) { |
michael@0 | 1006 | if (DEBUG) { |
michael@0 | 1007 | debug("Get alarms for " + aManifestURL); |
michael@0 | 1008 | } |
michael@0 | 1009 | |
michael@0 | 1010 | txn.result = []; |
michael@0 | 1011 | store.index("manifestURL").openCursor(aManifestURL) |
michael@0 | 1012 | .onsuccess = function onsuccess(event) { |
michael@0 | 1013 | let cursor = event.target.result; |
michael@0 | 1014 | if (!cursor) { |
michael@0 | 1015 | return; |
michael@0 | 1016 | } |
michael@0 | 1017 | |
michael@0 | 1018 | if (!aNetworkId || cursor.value.networkId == aNetworkId) { |
michael@0 | 1019 | txn.result.push(self.recordToAlarm(cursor.value)); |
michael@0 | 1020 | } |
michael@0 | 1021 | |
michael@0 | 1022 | cursor.continue(); |
michael@0 | 1023 | } |
michael@0 | 1024 | }, aResultCb); |
michael@0 | 1025 | }, |
michael@0 | 1026 | |
michael@0 | 1027 | _resetAlarms: function _resetAlarms(aNetworkId, aResultCb) { |
michael@0 | 1028 | this.dbNewTxn(ALARMS_STORE_NAME, "readwrite", function(txn, store) { |
michael@0 | 1029 | if (DEBUG) { |
michael@0 | 1030 | debug("Reset alarms for network " + aNetworkId); |
michael@0 | 1031 | } |
michael@0 | 1032 | |
michael@0 | 1033 | let lowerFilter = [aNetworkId, 0]; |
michael@0 | 1034 | let upperFilter = [aNetworkId, ""]; |
michael@0 | 1035 | let range = IDBKeyRange.bound(lowerFilter, upperFilter); |
michael@0 | 1036 | |
michael@0 | 1037 | store.index("alarm").openCursor(range).onsuccess = function onsuccess(event) { |
michael@0 | 1038 | let cursor = event.target.result; |
michael@0 | 1039 | if (cursor) { |
michael@0 | 1040 | if (cursor.value.startTime) { |
michael@0 | 1041 | cursor.value.relativeThreshold = cursor.value.threshold; |
michael@0 | 1042 | cursor.update(cursor.value); |
michael@0 | 1043 | } |
michael@0 | 1044 | cursor.continue(); |
michael@0 | 1045 | return; |
michael@0 | 1046 | } |
michael@0 | 1047 | }; |
michael@0 | 1048 | }, aResultCb); |
michael@0 | 1049 | } |
michael@0 | 1050 | }; |