1/* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this file, 3 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5"use strict"; 6 7this.EXPORTED_SYMBOLS = ['NetworkStatsDB']; 8 9const DEBUG = false; 10function debug(s) { dump("-*- NetworkStatsDB: " + s + "\n"); } 11 12const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; 13 14Cu.import("resource://gre/modules/XPCOMUtils.jsm"); 15Cu.import("resource://gre/modules/Services.jsm"); 16Cu.import("resource://gre/modules/IndexedDBHelper.jsm"); 17Cu.importGlobalProperties(["indexedDB"]); 18 19XPCOMUtils.defineLazyServiceGetter(this, "appsService", 20 "@mozilla.org/AppsService;1", 21 "nsIAppsService"); 22 23const DB_NAME = "net_stats"; 24const DB_VERSION = 9; 25const DEPRECATED_STATS_STORE_NAME = 26 [ 27 "net_stats_v2", // existed only in DB version 2 28 "net_stats", // existed in DB version 1 and 3 to 5 29 "net_stats_store", // existed in DB version 6 to 8 30 ]; 31const STATS_STORE_NAME = "net_stats_store_v3"; // since DB version 9 32const ALARMS_STORE_NAME = "net_alarm"; 33 34// Constant defining the maximum values allowed per interface. If more, older 35// will be erased. 36const VALUES_MAX_LENGTH = 6 * 30; 37 38// Constant defining the rate of the samples. Daily. 39const SAMPLE_RATE = 1000 * 60 * 60 * 24; 40 41this.NetworkStatsDB = function NetworkStatsDB() { 42 if (DEBUG) { 43 debug("Constructor"); 44 } 45 this.initDBHelper(DB_NAME, DB_VERSION, [STATS_STORE_NAME, ALARMS_STORE_NAME]); 46} 47 48NetworkStatsDB.prototype = { 49 __proto__: IndexedDBHelper.prototype, 50 51 dbNewTxn: function dbNewTxn(store_name, txn_type, callback, txnCb) { 52 function successCb(result) { 53 txnCb(null, result); 54 } 55 function errorCb(error) { 56 txnCb(error, null); 57 } 58 return this.newTxn(txn_type, store_name, callback, successCb, errorCb); 59 }, 60 61 /** 62 * The onupgradeneeded handler of the IDBOpenDBRequest. 63 * This function is called in IndexedDBHelper open() method. 64 * 65 * @param {IDBTransaction} aTransaction 66 * {IDBDatabase} aDb 67 * {64-bit integer} aOldVersion The version number on local storage. 68 * {64-bit integer} aNewVersion The version number to be upgraded to. 69 * 70 * @note Be careful with the database upgrade pattern. 71 * Because IndexedDB operations are performed asynchronously, we must 72 * apply a recursive approach instead of an iterative approach while 73 * upgrading versions. 74 */ 75 upgradeSchema: function upgradeSchema(aTransaction, aDb, aOldVersion, aNewVersion) { 76 if (DEBUG) { 77 debug("upgrade schema from: " + aOldVersion + " to " + aNewVersion + " called!"); 78 } 79 let db = aDb; 80 let objectStore; 81 82 // An array of upgrade functions for each version. 83 let upgradeSteps = [ 84 function upgrade0to1() { 85 if (DEBUG) debug("Upgrade 0 to 1: Create object stores and indexes."); 86 87 // Create the initial database schema. 88 objectStore = db.createObjectStore(DEPRECATED_STATS_STORE_NAME[1], 89 { keyPath: ["connectionType", "timestamp"] }); 90 objectStore.createIndex("connectionType", "connectionType", { unique: false }); 91 objectStore.createIndex("timestamp", "timestamp", { unique: false }); 92 objectStore.createIndex("rxBytes", "rxBytes", { unique: false }); 93 objectStore.createIndex("txBytes", "txBytes", { unique: false }); 94 objectStore.createIndex("rxTotalBytes", "rxTotalBytes", { unique: false }); 95 objectStore.createIndex("txTotalBytes", "txTotalBytes", { unique: false }); 96 97 upgradeNextVersion(); 98 }, 99 100 function upgrade1to2() { 101 if (DEBUG) debug("Upgrade 1 to 2: Do nothing."); 102 upgradeNextVersion(); 103 }, 104 105 function upgrade2to3() { 106 if (DEBUG) debug("Upgrade 2 to 3: Add keyPath appId to object store."); 107 108 // In order to support per-app traffic data storage, the original 109 // objectStore needs to be replaced by a new objectStore with new 110 // key path ("appId") and new index ("appId"). 111 // Also, since now networks are identified by their 112 // [networkId, networkType] not just by their connectionType, 113 // to modify the keyPath is mandatory to delete the object store 114 // and create it again. Old data is going to be deleted because the 115 // networkId for each sample can not be set. 116 117 // In version 1.2 objectStore name was 'net_stats_v2', to avoid errors when 118 // upgrading from 1.2 to 1.3 objectStore name should be checked. 119 let stores = db.objectStoreNames; 120 let deprecatedName = DEPRECATED_STATS_STORE_NAME[0]; 121 let storeName = DEPRECATED_STATS_STORE_NAME[1]; 122 if(stores.contains(deprecatedName)) { 123 // Delete the obsolete stats store. 124 db.deleteObjectStore(deprecatedName); 125 } else { 126 // Re-create stats object store without copying records. 127 db.deleteObjectStore(storeName); 128 } 129 130 objectStore = db.createObjectStore(storeName, { keyPath: ["appId", "network", "timestamp"] }); 131 objectStore.createIndex("appId", "appId", { unique: false }); 132 objectStore.createIndex("network", "network", { unique: false }); 133 objectStore.createIndex("networkType", "networkType", { unique: false }); 134 objectStore.createIndex("timestamp", "timestamp", { unique: false }); 135 objectStore.createIndex("rxBytes", "rxBytes", { unique: false }); 136 objectStore.createIndex("txBytes", "txBytes", { unique: false }); 137 objectStore.createIndex("rxTotalBytes", "rxTotalBytes", { unique: false }); 138 objectStore.createIndex("txTotalBytes", "txTotalBytes", { unique: false }); 139 140 upgradeNextVersion(); 141 }, 142 143 function upgrade3to4() { 144 if (DEBUG) debug("Upgrade 3 to 4: Delete redundant indexes."); 145 146 // Delete redundant indexes (leave "network" only). 147 objectStore = aTransaction.objectStore(DEPRECATED_STATS_STORE_NAME[1]); 148 if (objectStore.indexNames.contains("appId")) { 149 objectStore.deleteIndex("appId"); 150 } 151 if (objectStore.indexNames.contains("networkType")) { 152 objectStore.deleteIndex("networkType"); 153 } 154 if (objectStore.indexNames.contains("timestamp")) { 155 objectStore.deleteIndex("timestamp"); 156 } 157 if (objectStore.indexNames.contains("rxBytes")) { 158 objectStore.deleteIndex("rxBytes"); 159 } 160 if (objectStore.indexNames.contains("txBytes")) { 161 objectStore.deleteIndex("txBytes"); 162 } 163 if (objectStore.indexNames.contains("rxTotalBytes")) { 164 objectStore.deleteIndex("rxTotalBytes"); 165 } 166 if (objectStore.indexNames.contains("txTotalBytes")) { 167 objectStore.deleteIndex("txTotalBytes"); 168 } 169 170 upgradeNextVersion(); 171 }, 172 173 function upgrade4to5() { 174 if (DEBUG) debug("Upgrade 4 to 5: Create object store for alarms."); 175 176 // In order to manage alarms, it is necessary to use a global counter 177 // (totalBytes) that will increase regardless of the system reboot. 178 objectStore = aTransaction.objectStore(DEPRECATED_STATS_STORE_NAME[1]); 179 180 // Now, systemBytes will hold the old totalBytes and totalBytes will 181 // keep the increasing counter. |counters| will keep the track of 182 // accumulated values. 183 let counters = {}; 184 185 objectStore.openCursor().onsuccess = function(event) { 186 let cursor = event.target.result; 187 if (!cursor){ 188 // upgrade4to5 completed now. 189 upgradeNextVersion(); 190 return; 191 } 192 193 cursor.value.rxSystemBytes = cursor.value.rxTotalBytes; 194 cursor.value.txSystemBytes = cursor.value.txTotalBytes; 195 196 if (cursor.value.appId == 0) { 197 let netId = cursor.value.network[0] + '' + cursor.value.network[1]; 198 if (!counters[netId]) { 199 counters[netId] = { 200 rxCounter: 0, 201 txCounter: 0, 202 lastRx: 0, 203 lastTx: 0 204 }; 205 } 206 207 let rxDiff = cursor.value.rxSystemBytes - counters[netId].lastRx; 208 let txDiff = cursor.value.txSystemBytes - counters[netId].lastTx; 209 if (rxDiff < 0 || txDiff < 0) { 210 // System reboot between samples, so take the current one. 211 rxDiff = cursor.value.rxSystemBytes; 212 txDiff = cursor.value.txSystemBytes; 213 } 214 215 counters[netId].rxCounter += rxDiff; 216 counters[netId].txCounter += txDiff; 217 cursor.value.rxTotalBytes = counters[netId].rxCounter; 218 cursor.value.txTotalBytes = counters[netId].txCounter; 219 220 counters[netId].lastRx = cursor.value.rxSystemBytes; 221 counters[netId].lastTx = cursor.value.txSystemBytes; 222 } else { 223 cursor.value.rxTotalBytes = cursor.value.rxSystemBytes; 224 cursor.value.txTotalBytes = cursor.value.txSystemBytes; 225 } 226 227 cursor.update(cursor.value); 228 cursor.continue(); 229 }; 230 231 // Create object store for alarms. 232 objectStore = db.createObjectStore(ALARMS_STORE_NAME, { keyPath: "id", autoIncrement: true }); 233 objectStore.createIndex("alarm", ['networkId','threshold'], { unique: false }); 234 objectStore.createIndex("manifestURL", "manifestURL", { unique: false }); 235 }, 236 237 function upgrade5to6() { 238 if (DEBUG) debug("Upgrade 5 to 6: Add keyPath serviceType to object store."); 239 240 // In contrast to "per-app" traffic data, "system-only" traffic data 241 // refers to data which can not be identified by any applications. 242 // To further support "system-only" data storage, the data can be 243 // saved by service type (e.g., Tethering, OTA). Thus it's needed to 244 // have a new key ("serviceType") for the ojectStore. 245 let newObjectStore; 246 let deprecatedName = DEPRECATED_STATS_STORE_NAME[1]; 247 newObjectStore = db.createObjectStore(DEPRECATED_STATS_STORE_NAME[2], 248 { keyPath: ["appId", "serviceType", "network", "timestamp"] }); 249 newObjectStore.createIndex("network", "network", { unique: false }); 250 251 // Copy the data from the original objectStore to the new objectStore. 252 objectStore = aTransaction.objectStore(deprecatedName); 253 objectStore.openCursor().onsuccess = function(event) { 254 let cursor = event.target.result; 255 if (!cursor) { 256 db.deleteObjectStore(deprecatedName); 257 // upgrade5to6 completed now. 258 upgradeNextVersion(); 259 return; 260 } 261 262 let newStats = cursor.value; 263 newStats.serviceType = ""; 264 newObjectStore.put(newStats); 265 cursor.continue(); 266 }; 267 }, 268 269 function upgrade6to7() { 270 if (DEBUG) debug("Upgrade 6 to 7: Replace alarm threshold by relativeThreshold."); 271 272 // Replace threshold attribute of alarm index by relativeThreshold in alarms DB. 273 // Now alarms are indexed by relativeThreshold, which is the threshold relative 274 // to current system stats. 275 let alarmsStore = aTransaction.objectStore(ALARMS_STORE_NAME); 276 277 // Delete "alarm" index. 278 if (alarmsStore.indexNames.contains("alarm")) { 279 alarmsStore.deleteIndex("alarm"); 280 } 281 282 // Create new "alarm" index. 283 alarmsStore.createIndex("alarm", ['networkId','relativeThreshold'], { unique: false }); 284 285 // Populate new "alarm" index attributes. 286 alarmsStore.openCursor().onsuccess = function(event) { 287 let cursor = event.target.result; 288 if (!cursor) { 289 upgrade6to7_updateTotalBytes(); 290 return; 291 } 292 293 cursor.value.relativeThreshold = cursor.value.threshold; 294 cursor.value.absoluteThreshold = cursor.value.threshold; 295 delete cursor.value.threshold; 296 297 cursor.update(cursor.value); 298 cursor.continue(); 299 } 300 301 function upgrade6to7_updateTotalBytes() { 302 if (DEBUG) debug("Upgrade 6 to 7: Update TotalBytes."); 303 // Previous versions save accumulative totalBytes, increasing although the system 304 // reboots or resets stats. But is necessary to reset the total counters when reset 305 // through 'clearInterfaceStats'. 306 let statsStore = aTransaction.objectStore(DEPRECATED_STATS_STORE_NAME[2]); 307 let networks = []; 308 309 // Find networks stored in the database. 310 statsStore.index("network").openKeyCursor(null, "nextunique").onsuccess = function(event) { 311 let cursor = event.target.result; 312 313 // Store each network into an array. 314 if (cursor) { 315 networks.push(cursor.key); 316 cursor.continue(); 317 return; 318 } 319 320 // Start to deal with each network. 321 let pending = networks.length; 322 323 if (pending === 0) { 324 // Found no records of network. upgrade6to7 completed now. 325 upgradeNextVersion(); 326 return; 327 } 328 329 networks.forEach(function(network) { 330 let lowerFilter = [0, "", network, 0]; 331 let upperFilter = [0, "", network, ""]; 332 let range = IDBKeyRange.bound(lowerFilter, upperFilter, false, false); 333 334 // Find number of samples for a given network. 335 statsStore.count(range).onsuccess = function(event) { 336 let recordCount = event.target.result; 337 338 // If there are more samples than the max allowed, there is no way to know 339 // when does reset take place. 340 if (recordCount === 0 || recordCount >= VALUES_MAX_LENGTH) { 341 pending--; 342 if (pending === 0) { 343 upgradeNextVersion(); 344 } 345 return; 346 } 347 348 let last = null; 349 // Reset detected if the first sample totalCounters are different than bytes 350 // counters. If so, the total counters should be recalculated. 351 statsStore.openCursor(range).onsuccess = function(event) { 352 let cursor = event.target.result; 353 if (!cursor) { 354 pending--; 355 if (pending === 0) { 356 upgradeNextVersion(); 357 } 358 return; 359 } 360 if (!last) { 361 if (cursor.value.rxTotalBytes == cursor.value.rxBytes && 362 cursor.value.txTotalBytes == cursor.value.txBytes) { 363 pending--; 364 if (pending === 0) { 365 upgradeNextVersion(); 366 } 367 return; 368 } 369 370 cursor.value.rxTotalBytes = cursor.value.rxBytes; 371 cursor.value.txTotalBytes = cursor.value.txBytes; 372 cursor.update(cursor.value); 373 last = cursor.value; 374 cursor.continue(); 375 return; 376 } 377 378 // Recalculate the total counter for last / current sample 379 cursor.value.rxTotalBytes = last.rxTotalBytes + cursor.value.rxBytes; 380 cursor.value.txTotalBytes = last.txTotalBytes + cursor.value.txBytes; 381 cursor.update(cursor.value); 382 last = cursor.value; 383 cursor.continue(); 384 } 385 } 386 }, this); // end of networks.forEach() 387 }; // end of statsStore.index("network").openKeyCursor().onsuccess callback 388 } // end of function upgrade6to7_updateTotalBytes 389 }, 390 391 function upgrade7to8() { 392 if (DEBUG) debug("Upgrade 7 to 8: Create index serviceType."); 393 394 // Create index for 'ServiceType' in order to make it retrievable. 395 let statsStore = aTransaction.objectStore(DEPRECATED_STATS_STORE_NAME[2]); 396 statsStore.createIndex("serviceType", "serviceType", { unique: false }); 397 398 upgradeNextVersion(); 399 }, 400 401 function upgrade8to9() { 402 if (DEBUG) debug("Upgrade 8 to 9: Add keyPath isInBrowser to " + 403 "network stats object store"); 404 405 // Since B2G v2.0, there is no stand-alone browser app anymore. 406 // The browser app is a mozbrowser iframe element owned by system app. 407 // In order to separate traffic generated from system and browser, we 408 // have to add a new attribute |isInBrowser| as keyPath. 409 // Refer to bug 1070944 for more detail. 410 let newObjectStore; 411 let deprecatedName = DEPRECATED_STATS_STORE_NAME[2]; 412 newObjectStore = db.createObjectStore(STATS_STORE_NAME, 413 { keyPath: ["appId", "isInBrowser", "serviceType", 414 "network", "timestamp"] }); 415 newObjectStore.createIndex("network", "network", { unique: false }); 416 newObjectStore.createIndex("serviceType", "serviceType", { unique: false }); 417 418 // Copy records from the current object store to the new one. 419 objectStore = aTransaction.objectStore(deprecatedName); 420 objectStore.openCursor().onsuccess = function (event) { 421 let cursor = event.target.result; 422 if (!cursor) { 423 db.deleteObjectStore(deprecatedName); 424 // upgrade8to9 completed now. 425 return; 426 } 427 let newStats = cursor.value; 428 // Augment records by adding the new isInBrowser attribute. 429 // Notes: 430 // 1. Key value cannot be boolean type. Use 1/0 instead of true/false. 431 // 2. Most traffic of system app should come from its browser iframe, 432 // thus assign isInBrowser as 1 for system app. 433 let manifestURL = appsService.getManifestURLByLocalId(newStats.appId); 434 if (manifestURL && manifestURL.search(/app:\/\/system\./) === 0) { 435 newStats.isInBrowser = 1; 436 } else { 437 newStats.isInBrowser = 0; 438 } 439 newObjectStore.put(newStats); 440 cursor.continue(); 441 }; 442 } 443 ]; 444 445 let index = aOldVersion; 446 let outer = this; 447 448 function upgradeNextVersion() { 449 if (index == aNewVersion) { 450 debug("Upgrade finished."); 451 return; 452 } 453 454 try { 455 var i = index++; 456 if (DEBUG) debug("Upgrade step: " + i + "\n"); 457 upgradeSteps[i].call(outer); 458 } catch (ex) { 459 dump("Caught exception " + ex); 460 throw ex; 461 return; 462 } 463 } 464 465 if (aNewVersion > upgradeSteps.length) { 466 debug("No migration steps for the new version!"); 467 aTransaction.abort(); 468 return; 469 } 470 471 upgradeNextVersion(); 472 }, 473 474 importData: function importData(aStats) { 475 let stats = { appId: aStats.appId, 476 isInBrowser: aStats.isInBrowser ? 1 : 0, 477 serviceType: aStats.serviceType, 478 network: [aStats.networkId, aStats.networkType], 479 timestamp: aStats.timestamp, 480 rxBytes: aStats.rxBytes, 481 txBytes: aStats.txBytes, 482 rxSystemBytes: aStats.rxSystemBytes, 483 txSystemBytes: aStats.txSystemBytes, 484 rxTotalBytes: aStats.rxTotalBytes, 485 txTotalBytes: aStats.txTotalBytes }; 486 487 return stats; 488 }, 489 490 exportData: function exportData(aStats) { 491 let stats = { appId: aStats.appId, 492 isInBrowser: aStats.isInBrowser ? true : false, 493 serviceType: aStats.serviceType, 494 networkId: aStats.network[0], 495 networkType: aStats.network[1], 496 timestamp: aStats.timestamp, 497 rxBytes: aStats.rxBytes, 498 txBytes: aStats.txBytes, 499 rxTotalBytes: aStats.rxTotalBytes, 500 txTotalBytes: aStats.txTotalBytes }; 501 502 return stats; 503 }, 504 505 normalizeDate: function normalizeDate(aDate) { 506 // Convert to UTC according to timezone and 507 // filter timestamp to get SAMPLE_RATE precission 508 let timestamp = aDate.getTime() - aDate.getTimezoneOffset() * 60 * 1000; 509 timestamp = Math.floor(timestamp / SAMPLE_RATE) * SAMPLE_RATE; 510 return timestamp; 511 }, 512 513 saveStats: function saveStats(aStats, aResultCb) { 514 let isAccumulative = aStats.isAccumulative; 515 let timestamp = this.normalizeDate(aStats.date); 516 517 let stats = { appId: aStats.appId, 518 isInBrowser: aStats.isInBrowser, 519 serviceType: aStats.serviceType, 520 networkId: aStats.networkId, 521 networkType: aStats.networkType, 522 timestamp: timestamp, 523 rxBytes: isAccumulative ? 0 : aStats.rxBytes, 524 txBytes: isAccumulative ? 0 : aStats.txBytes, 525 rxSystemBytes: isAccumulative ? aStats.rxBytes : 0, 526 txSystemBytes: isAccumulative ? aStats.txBytes : 0, 527 rxTotalBytes: isAccumulative ? aStats.rxBytes : 0, 528 txTotalBytes: isAccumulative ? aStats.txBytes : 0 }; 529 530 stats = this.importData(stats); 531 532 this.dbNewTxn(STATS_STORE_NAME, "readwrite", function(aTxn, aStore) { 533 if (DEBUG) { 534 debug("Filtered time: " + new Date(timestamp)); 535 debug("New stats: " + JSON.stringify(stats)); 536 } 537 538 let lowerFilter = [stats.appId, stats.isInBrowser, stats.serviceType, 539 stats.network, 0]; 540 let upperFilter = [stats.appId, stats.isInBrowser, stats.serviceType, 541 stats.network, ""]; 542 let range = IDBKeyRange.bound(lowerFilter, upperFilter, false, false); 543 544 let request = aStore.openCursor(range, 'prev'); 545 request.onsuccess = function onsuccess(event) { 546 let cursor = event.target.result; 547 if (!cursor) { 548 // Empty, so save first element. 549 550 if (!isAccumulative) { 551 this._saveStats(aTxn, aStore, stats); 552 return; 553 } 554 555 // There could be a time delay between the point when the network 556 // interface comes up and the point when the database is initialized. 557 // In this short interval some traffic data are generated but are not 558 // registered by the first sample. 559 stats.rxBytes = stats.rxTotalBytes; 560 stats.txBytes = stats.txTotalBytes; 561 562 // However, if the interface is not switched on after the database is 563 // initialized (dual sim use case) stats should be set to 0. 564 let req = aStore.index("network").openKeyCursor(null, "nextunique"); 565 req.onsuccess = function onsuccess(event) { 566 let cursor = event.target.result; 567 if (cursor) { 568 if (cursor.key[1] == stats.network[1]) { 569 stats.rxBytes = 0; 570 stats.txBytes = 0; 571 this._saveStats(aTxn, aStore, stats); 572 return; 573 } 574 575 cursor.continue(); 576 return; 577 } 578 579 this._saveStats(aTxn, aStore, stats); 580 }.bind(this); 581 582 return; 583 } 584 585 // There are old samples 586 if (DEBUG) { 587 debug("Last value " + JSON.stringify(cursor.value)); 588 } 589 590 // Remove stats previous to now - VALUE_MAX_LENGTH 591 this._removeOldStats(aTxn, aStore, stats.appId, stats.isInBrowser, 592 stats.serviceType, stats.network, stats.timestamp); 593 594 // Process stats before save 595 this._processSamplesDiff(aTxn, aStore, cursor, stats, isAccumulative); 596 }.bind(this); 597 }.bind(this), aResultCb); 598 }, 599 600 /* 601 * This function check that stats are saved in the database following the sample rate. 602 * In this way is easier to find elements when stats are requested. 603 */ 604 _processSamplesDiff: function _processSamplesDiff(aTxn, 605 aStore, 606 aLastSampleCursor, 607 aNewSample, 608 aIsAccumulative) { 609 let lastSample = aLastSampleCursor.value; 610 611 // Get difference between last and new sample. 612 let diff = (aNewSample.timestamp - lastSample.timestamp) / SAMPLE_RATE; 613 if (diff % 1) { 614 // diff is decimal, so some error happened because samples are stored as a multiple 615 // of SAMPLE_RATE 616 aTxn.abort(); 617 throw new Error("Error processing samples"); 618 } 619 620 if (DEBUG) { 621 debug("New: " + aNewSample.timestamp + " - Last: " + 622 lastSample.timestamp + " - diff: " + diff); 623 } 624 625 // If the incoming data has a accumulation feature, the new 626 // |txBytes|/|rxBytes| is assigend by differnces between the new 627 // |txTotalBytes|/|rxTotalBytes| and the last |txTotalBytes|/|rxTotalBytes|. 628 // Else, if incoming data is non-accumulative, the |txBytes|/|rxBytes| 629 // is the new |txBytes|/|rxBytes|. 630 let rxDiff = 0; 631 let txDiff = 0; 632 if (aIsAccumulative) { 633 rxDiff = aNewSample.rxSystemBytes - lastSample.rxSystemBytes; 634 txDiff = aNewSample.txSystemBytes - lastSample.txSystemBytes; 635 if (rxDiff < 0 || txDiff < 0) { 636 rxDiff = aNewSample.rxSystemBytes; 637 txDiff = aNewSample.txSystemBytes; 638 } 639 aNewSample.rxBytes = rxDiff; 640 aNewSample.txBytes = txDiff; 641 642 aNewSample.rxTotalBytes = lastSample.rxTotalBytes + rxDiff; 643 aNewSample.txTotalBytes = lastSample.txTotalBytes + txDiff; 644 } else { 645 rxDiff = aNewSample.rxBytes; 646 txDiff = aNewSample.txBytes; 647 } 648 649 if (diff == 1) { 650 // New element. 651 652 // If the incoming data is non-accumulative, the new 653 // |rxTotalBytes|/|txTotalBytes| needs to be updated by adding new 654 // |rxBytes|/|txBytes| to the last |rxTotalBytes|/|txTotalBytes|. 655 if (!aIsAccumulative) { 656 aNewSample.rxTotalBytes = aNewSample.rxBytes + lastSample.rxTotalBytes; 657 aNewSample.txTotalBytes = aNewSample.txBytes + lastSample.txTotalBytes; 658 } 659 660 this._saveStats(aTxn, aStore, aNewSample); 661 return; 662 } 663 if (diff > 1) { 664 // Some samples lost. Device off during one or more samplerate periods. 665 // Time or timezone changed 666 // Add lost samples with 0 bytes and the actual one. 667 if (diff > VALUES_MAX_LENGTH) { 668 diff = VALUES_MAX_LENGTH; 669 } 670 671 let data = []; 672 for (let i = diff - 2; i >= 0; i--) { 673 let time = aNewSample.timestamp - SAMPLE_RATE * (i + 1); 674 let sample = { appId: aNewSample.appId, 675 isInBrowser: aNewSample.isInBrowser, 676 serviceType: aNewSample.serviceType, 677 network: aNewSample.network, 678 timestamp: time, 679 rxBytes: 0, 680 txBytes: 0, 681 rxSystemBytes: lastSample.rxSystemBytes, 682 txSystemBytes: lastSample.txSystemBytes, 683 rxTotalBytes: lastSample.rxTotalBytes, 684 txTotalBytes: lastSample.txTotalBytes }; 685 686 data.push(sample); 687 } 688 689 data.push(aNewSample); 690 this._saveStats(aTxn, aStore, data); 691 return; 692 } 693 if (diff == 0 || diff < 0) { 694 // New element received before samplerate period. It means that device has 695 // been restarted (or clock / timezone change). 696 // Update element. If diff < 0, clock or timezone changed back. Place data 697 // in the last sample. 698 699 // Old |rxTotalBytes|/|txTotalBytes| needs to get updated by adding the 700 // last |rxTotalBytes|/|txTotalBytes|. 701 lastSample.rxBytes += rxDiff; 702 lastSample.txBytes += txDiff; 703 lastSample.rxSystemBytes = aNewSample.rxSystemBytes; 704 lastSample.txSystemBytes = aNewSample.txSystemBytes; 705 lastSample.rxTotalBytes += rxDiff; 706 lastSample.txTotalBytes += txDiff; 707 708 if (DEBUG) { 709 debug("Update: " + JSON.stringify(lastSample)); 710 } 711 let req = aLastSampleCursor.update(lastSample); 712 } 713 }, 714 715 _saveStats: function _saveStats(aTxn, aStore, aNetworkStats) { 716 if (DEBUG) { 717 debug("_saveStats: " + JSON.stringify(aNetworkStats)); 718 } 719 720 if (Array.isArray(aNetworkStats)) { 721 let len = aNetworkStats.length - 1; 722 for (let i = 0; i <= len; i++) { 723 aStore.put(aNetworkStats[i]); 724 } 725 } else { 726 aStore.put(aNetworkStats); 727 } 728 }, 729 730 _removeOldStats: function _removeOldStats(aTxn, aStore, aAppId, aIsInBrowser, 731 aServiceType, aNetwork, aDate) { 732 // Callback function to remove old items when new ones are added. 733 let filterDate = aDate - (SAMPLE_RATE * VALUES_MAX_LENGTH - 1); 734 let lowerFilter = [aAppId, aIsInBrowser, aServiceType, aNetwork, 0]; 735 let upperFilter = [aAppId, aIsInBrowser, aServiceType, aNetwork, filterDate]; 736 let range = IDBKeyRange.bound(lowerFilter, upperFilter, false, false); 737 let lastSample = null; 738 let self = this; 739 740 aStore.openCursor(range).onsuccess = function(event) { 741 var cursor = event.target.result; 742 if (cursor) { 743 lastSample = cursor.value; 744 cursor.delete(); 745 cursor.continue(); 746 return; 747 } 748 749 // If all samples for a network are removed, an empty sample 750 // has to be saved to keep the totalBytes in order to compute 751 // future samples because system counters are not set to 0. 752 // Thus, if there are no samples left, the last sample removed 753 // will be saved again after setting its bytes to 0. 754 let request = aStore.index("network").openCursor(aNetwork); 755 request.onsuccess = function onsuccess(event) { 756 let cursor = event.target.result; 757 if (!cursor && lastSample != null) { 758 let timestamp = new Date(); 759 timestamp = self.normalizeDate(timestamp); 760 lastSample.timestamp = timestamp; 761 lastSample.rxBytes = 0; 762 lastSample.txBytes = 0; 763 self._saveStats(aTxn, aStore, lastSample); 764 } 765 }; 766 }; 767 }, 768 769 clearInterfaceStats: function clearInterfaceStats(aNetwork, aResultCb) { 770 let network = [aNetwork.network.id, aNetwork.network.type]; 771 let self = this; 772 773 // Clear and save an empty sample to keep sync with system counters 774 this.dbNewTxn(STATS_STORE_NAME, "readwrite", function(aTxn, aStore) { 775 let sample = null; 776 let request = aStore.index("network").openCursor(network, "prev"); 777 request.onsuccess = function onsuccess(event) { 778 let cursor = event.target.result; 779 if (cursor) { 780 if (!sample && cursor.value.appId == 0) { 781 sample = cursor.value; 782 } 783 784 cursor.delete(); 785 cursor.continue(); 786 return; 787 } 788 789 if (sample) { 790 let timestamp = new Date(); 791 timestamp = self.normalizeDate(timestamp); 792 sample.timestamp = timestamp; 793 sample.appId = 0; 794 sample.isInBrowser = 0; 795 sample.serviceType = ""; 796 sample.rxBytes = 0; 797 sample.txBytes = 0; 798 sample.rxTotalBytes = 0; 799 sample.txTotalBytes = 0; 800 801 self._saveStats(aTxn, aStore, sample); 802 } 803 }; 804 }, this._resetAlarms.bind(this, aNetwork.networkId, aResultCb)); 805 }, 806 807 clearStats: function clearStats(aNetworks, aResultCb) { 808 let index = 0; 809 let stats = []; 810 let self = this; 811 812 let callback = function(aError, aResult) { 813 index++; 814 815 if (!aError && index < aNetworks.length) { 816 self.clearInterfaceStats(aNetworks[index], callback); 817 return; 818 } 819 820 aResultCb(aError, aResult); 821 }; 822 823 if (!aNetworks[index]) { 824 aResultCb(null, true); 825 return; 826 } 827 this.clearInterfaceStats(aNetworks[index], callback); 828 }, 829 830 getCurrentStats: function getCurrentStats(aNetwork, aDate, aResultCb) { 831 if (DEBUG) { 832 debug("Get current stats for " + JSON.stringify(aNetwork) + " since " + aDate); 833 } 834 835 let network = [aNetwork.id, aNetwork.type]; 836 if (aDate) { 837 this._getCurrentStatsFromDate(network, aDate, aResultCb); 838 return; 839 } 840 841 this._getCurrentStats(network, aResultCb); 842 }, 843 844 _getCurrentStats: function _getCurrentStats(aNetwork, aResultCb) { 845 this.dbNewTxn(STATS_STORE_NAME, "readonly", function(txn, store) { 846 let request = null; 847 let upperFilter = [0, 1, "", aNetwork, Date.now()]; 848 let range = IDBKeyRange.upperBound(upperFilter, false); 849 let result = { rxBytes: 0, txBytes: 0, 850 rxTotalBytes: 0, txTotalBytes: 0 }; 851 852 request = store.openCursor(range, "prev"); 853 854 request.onsuccess = function onsuccess(event) { 855 let cursor = event.target.result; 856 if (cursor) { 857 result.rxBytes = result.rxTotalBytes = cursor.value.rxTotalBytes; 858 result.txBytes = result.txTotalBytes = cursor.value.txTotalBytes; 859 } 860 861 txn.result = result; 862 }; 863 }.bind(this), aResultCb); 864 }, 865 866 _getCurrentStatsFromDate: function _getCurrentStatsFromDate(aNetwork, aDate, aResultCb) { 867 aDate = new Date(aDate); 868 this.dbNewTxn(STATS_STORE_NAME, "readonly", function(txn, store) { 869 let request = null; 870 let start = this.normalizeDate(aDate); 871 let upperFilter = [0, 1, "", aNetwork, Date.now()]; 872 let range = IDBKeyRange.upperBound(upperFilter, false); 873 let result = { rxBytes: 0, txBytes: 0, 874 rxTotalBytes: 0, txTotalBytes: 0 }; 875 876 request = store.openCursor(range, "prev"); 877 878 request.onsuccess = function onsuccess(event) { 879 let cursor = event.target.result; 880 if (cursor) { 881 result.rxBytes = result.rxTotalBytes = cursor.value.rxTotalBytes; 882 result.txBytes = result.txTotalBytes = cursor.value.txTotalBytes; 883 } 884 885 let timestamp = cursor.value.timestamp; 886 let range = IDBKeyRange.lowerBound(lowerFilter, false); 887 request = store.openCursor(range); 888 889 request.onsuccess = function onsuccess(event) { 890 let cursor = event.target.result; 891 if (cursor) { 892 if (cursor.value.timestamp == timestamp) { 893 // There is one sample only. 894 result.rxBytes = cursor.value.rxBytes; 895 result.txBytes = cursor.value.txBytes; 896 } else { 897 result.rxBytes -= cursor.value.rxTotalBytes; 898 result.txBytes -= cursor.value.txTotalBytes; 899 } 900 } 901 902 txn.result = result; 903 }; 904 }; 905 }.bind(this), aResultCb); 906 }, 907 908 find: function find(aResultCb, aAppId, aBrowsingTrafficOnly, aServiceType, 909 aNetwork, aStart, aEnd, aAppManifestURL) { 910 let offset = (new Date()).getTimezoneOffset() * 60 * 1000; 911 let start = this.normalizeDate(aStart); 912 let end = this.normalizeDate(aEnd); 913 914 if (DEBUG) { 915 debug("Find samples for appId: " + aAppId + 916 " browsingTrafficOnly: " + aBrowsingTrafficOnly + 917 " serviceType: " + aServiceType + 918 " network: " + JSON.stringify(aNetwork) + " from " + start + 919 " until " + end); 920 debug("Start time: " + new Date(start)); 921 debug("End time: " + new Date(end)); 922 } 923 924 // Find samples of browsing traffic (isInBrowser = 1) first since they are 925 // needed no matter browsingTrafficOnly is true or false. 926 // We have to make two queries to database because we cannot filter correct 927 // records by a single query that sets ranges for two keys (isInBrowser and 928 // timestamp). We think it is because the keyPath contains an array 929 // (network) so such query does not work. 930 this.dbNewTxn(STATS_STORE_NAME, "readonly", function(aTxn, aStore) { 931 let network = [aNetwork.id, aNetwork.type]; 932 let lowerFilter = [aAppId, 1, aServiceType, network, start]; 933 let upperFilter = [aAppId, 1, aServiceType, network, end]; 934 let range = IDBKeyRange.bound(lowerFilter, upperFilter, false, false); 935 936 let data = []; 937 938 if (!aTxn.result) { 939 aTxn.result = {}; 940 } 941 aTxn.result.appManifestURL = aAppManifestURL; 942 aTxn.result.browsingTrafficOnly = aBrowsingTrafficOnly; 943 aTxn.result.serviceType = aServiceType; 944 aTxn.result.network = aNetwork; 945 aTxn.result.start = aStart; 946 aTxn.result.end = aEnd; 947 948 let request = aStore.openCursor(range).onsuccess = function(event) { 949 var cursor = event.target.result; 950 if (cursor){ 951 // We use rxTotalBytes/txTotalBytes instead of rxBytes/txBytes for 952 // the first (oldest) sample. The rx/txTotalBytes fields record 953 // accumulative usage amount, which means even if old samples were 954 // expired and removed from the Database, we can still obtain the 955 // correct network usage. 956 if (data.length == 0) { 957 data.push({ rxBytes: cursor.value.rxTotalBytes, 958 txBytes: cursor.value.txTotalBytes, 959 date: new Date(cursor.value.timestamp + offset) }); 960 } else { 961 data.push({ rxBytes: cursor.value.rxBytes, 962 txBytes: cursor.value.txBytes, 963 date: new Date(cursor.value.timestamp + offset) }); 964 } 965 cursor.continue(); 966 return; 967 } 968 969 if (aBrowsingTrafficOnly) { 970 this.fillResultSamples(start + offset, end + offset, data); 971 aTxn.result.data = data; 972 return; 973 } 974 975 // Find samples of app traffic (isInBrowser = 0) as well if 976 // browsingTrafficOnly is false. 977 lowerFilter = [aAppId, 0, aServiceType, network, start]; 978 upperFilter = [aAppId, 0, aServiceType, network, end]; 979 range = IDBKeyRange.bound(lowerFilter, upperFilter, false, false); 980 request = aStore.openCursor(range).onsuccess = function(event) { 981 cursor = event.target.result; 982 if (cursor) { 983 var date = new Date(cursor.value.timestamp + offset); 984 var foundData = data.find(function (element, index, array) { 985 if (element.date.getTime() !== date.getTime()) { 986 return false; 987 } 988 return element; 989 }, date); 990 991 if (foundData) { 992 foundData.rxBytes += cursor.value.rxBytes; 993 foundData.txBytes += cursor.value.txBytes; 994 } else { 995 // We use rxTotalBytes/txTotalBytes instead of rxBytes/txBytes 996 // for the first (oldest) sample. The rx/txTotalBytes fields 997 // record accumulative usage amount, which means even if old 998 // samples were expired and removed from the Database, we can 999 // still obtain the correct network usage. 1000 if (data.length == 0) { 1001 data.push({ rxBytes: cursor.value.rxTotalBytes, 1002 txBytes: cursor.value.txTotalBytes, 1003 date: new Date(cursor.value.timestamp + offset) }); 1004 } else { 1005 data.push({ rxBytes: cursor.value.rxBytes, 1006 txBytes: cursor.value.txBytes, 1007 date: new Date(cursor.value.timestamp + offset) }); 1008 } 1009 } 1010 cursor.continue(); 1011 return; 1012 } 1013 this.fillResultSamples(start + offset, end + offset, data); 1014 aTxn.result.data = data; 1015 }.bind(this); // openCursor(range).onsuccess() callback 1016 }.bind(this); // openCursor(range).onsuccess() callback 1017 }.bind(this), aResultCb); 1018 }, 1019 1020 /* 1021 * Fill data array (samples from database) with empty samples to match 1022 * requested start / end dates. 1023 */ 1024 fillResultSamples: function fillResultSamples(aStart, aEnd, aData) { 1025 if (aData.length == 0) { 1026 aData.push({ rxBytes: undefined, 1027 txBytes: undefined, 1028 date: new Date(aStart) }); 1029 } 1030 1031 while (aStart < aData[0].date.getTime()) { 1032 aData.unshift({ rxBytes: undefined, 1033 txBytes: undefined, 1034 date: new Date(aData[0].date.getTime() - SAMPLE_RATE) }); 1035 } 1036 1037 while (aEnd > aData[aData.length - 1].date.getTime()) { 1038 aData.push({ rxBytes: undefined, 1039 txBytes: undefined, 1040 date: new Date(aData[aData.length - 1].date.getTime() + SAMPLE_RATE) }); 1041 } 1042 }, 1043 1044 getAvailableNetworks: function getAvailableNetworks(aResultCb) { 1045 this.dbNewTxn(STATS_STORE_NAME, "readonly", function(aTxn, aStore) { 1046 if (!aTxn.result) { 1047 aTxn.result = []; 1048 } 1049 1050 let request = aStore.index("network").openKeyCursor(null, "nextunique"); 1051 request.onsuccess = function onsuccess(event) { 1052 let cursor = event.target.result; 1053 if (cursor) { 1054 aTxn.result.push({ id: cursor.key[0], 1055 type: cursor.key[1] }); 1056 cursor.continue(); 1057 return; 1058 } 1059 }; 1060 }, aResultCb); 1061 }, 1062 1063 isNetworkAvailable: function isNetworkAvailable(aNetwork, aResultCb) { 1064 this.dbNewTxn(STATS_STORE_NAME, "readonly", function(aTxn, aStore) { 1065 if (!aTxn.result) { 1066 aTxn.result = false; 1067 } 1068 1069 let network = [aNetwork.id, aNetwork.type]; 1070 let request = aStore.index("network").openKeyCursor(IDBKeyRange.only(network)); 1071 request.onsuccess = function onsuccess(event) { 1072 if (event.target.result) { 1073 aTxn.result = true; 1074 } 1075 }; 1076 }, aResultCb); 1077 }, 1078 1079 getAvailableServiceTypes: function getAvailableServiceTypes(aResultCb) { 1080 this.dbNewTxn(STATS_STORE_NAME, "readonly", function(aTxn, aStore) { 1081 if (!aTxn.result) { 1082 aTxn.result = []; 1083 } 1084 1085 let request = aStore.index("serviceType").openKeyCursor(null, "nextunique"); 1086 request.onsuccess = function onsuccess(event) { 1087 let cursor = event.target.result; 1088 if (cursor && cursor.key != "") { 1089 aTxn.result.push({ serviceType: cursor.key }); 1090 cursor.continue(); 1091 return; 1092 } 1093 }; 1094 }, aResultCb); 1095 }, 1096 1097 get sampleRate () { 1098 return SAMPLE_RATE; 1099 }, 1100 1101 get maxStorageSamples () { 1102 return VALUES_MAX_LENGTH; 1103 }, 1104 1105 logAllRecords: function logAllRecords(aResultCb) { 1106 this.dbNewTxn(STATS_STORE_NAME, "readonly", function(aTxn, aStore) { 1107 aStore.mozGetAll().onsuccess = function onsuccess(event) { 1108 aTxn.result = event.target.result; 1109 }; 1110 }, aResultCb); 1111 }, 1112 1113 alarmToRecord: function alarmToRecord(aAlarm) { 1114 let record = { networkId: aAlarm.networkId, 1115 absoluteThreshold: aAlarm.absoluteThreshold, 1116 relativeThreshold: aAlarm.relativeThreshold, 1117 startTime: aAlarm.startTime, 1118 data: aAlarm.data, 1119 manifestURL: aAlarm.manifestURL, 1120 pageURL: aAlarm.pageURL }; 1121 1122 if (aAlarm.id) { 1123 record.id = aAlarm.id; 1124 } 1125 1126 return record; 1127 }, 1128 1129 recordToAlarm: function recordToalarm(aRecord) { 1130 let alarm = { networkId: aRecord.networkId, 1131 absoluteThreshold: aRecord.absoluteThreshold, 1132 relativeThreshold: aRecord.relativeThreshold, 1133 startTime: aRecord.startTime, 1134 data: aRecord.data, 1135 manifestURL: aRecord.manifestURL, 1136 pageURL: aRecord.pageURL }; 1137 1138 if (aRecord.id) { 1139 alarm.id = aRecord.id; 1140 } 1141 1142 return alarm; 1143 }, 1144 1145 addAlarm: function addAlarm(aAlarm, aResultCb) { 1146 this.dbNewTxn(ALARMS_STORE_NAME, "readwrite", function(txn, store) { 1147 if (DEBUG) { 1148 debug("Going to add " + JSON.stringify(aAlarm)); 1149 } 1150 1151 let record = this.alarmToRecord(aAlarm); 1152 store.put(record).onsuccess = function setResult(aEvent) { 1153 txn.result = aEvent.target.result; 1154 if (DEBUG) { 1155 debug("Request successful. New record ID: " + txn.result); 1156 } 1157 }; 1158 }.bind(this), aResultCb); 1159 }, 1160 1161 getFirstAlarm: function getFirstAlarm(aNetworkId, aResultCb) { 1162 let self = this; 1163 1164 this.dbNewTxn(ALARMS_STORE_NAME, "readonly", function(txn, store) { 1165 if (DEBUG) { 1166 debug("Get first alarm for network " + aNetworkId); 1167 } 1168 1169 let lowerFilter = [aNetworkId, 0]; 1170 let upperFilter = [aNetworkId, ""]; 1171 let range = IDBKeyRange.bound(lowerFilter, upperFilter); 1172 1173 store.index("alarm").openCursor(range).onsuccess = function onsuccess(event) { 1174 let cursor = event.target.result; 1175 txn.result = null; 1176 if (cursor) { 1177 txn.result = self.recordToAlarm(cursor.value); 1178 } 1179 }; 1180 }, aResultCb); 1181 }, 1182 1183 removeAlarm: function removeAlarm(aAlarmId, aManifestURL, aResultCb) { 1184 this.dbNewTxn(ALARMS_STORE_NAME, "readwrite", function(txn, store) { 1185 if (DEBUG) { 1186 debug("Remove alarm " + aAlarmId); 1187 } 1188 1189 store.get(aAlarmId).onsuccess = function onsuccess(event) { 1190 let record = event.target.result; 1191 txn.result = false; 1192 if (!record || (aManifestURL && record.manifestURL != aManifestURL)) { 1193 return; 1194 } 1195 1196 store.delete(aAlarmId); 1197 txn.result = true; 1198 } 1199 }, aResultCb); 1200 }, 1201 1202 removeAlarms: function removeAlarms(aManifestURL, aResultCb) { 1203 this.dbNewTxn(ALARMS_STORE_NAME, "readwrite", function(txn, store) { 1204 if (DEBUG) { 1205 debug("Remove alarms of " + aManifestURL); 1206 } 1207 1208 store.index("manifestURL").openCursor(aManifestURL) 1209 .onsuccess = function onsuccess(event) { 1210 let cursor = event.target.result; 1211 if (cursor) { 1212 cursor.delete(); 1213 cursor.continue(); 1214 } 1215 } 1216 }, aResultCb); 1217 }, 1218 1219 updateAlarm: function updateAlarm(aAlarm, aResultCb) { 1220 let self = this; 1221 this.dbNewTxn(ALARMS_STORE_NAME, "readwrite", function(txn, store) { 1222 if (DEBUG) { 1223 debug("Update alarm " + aAlarm.id); 1224 } 1225 1226 let record = self.alarmToRecord(aAlarm); 1227 store.openCursor(record.id).onsuccess = function onsuccess(event) { 1228 let cursor = event.target.result; 1229 txn.result = false; 1230 if (cursor) { 1231 cursor.update(record); 1232 txn.result = true; 1233 } 1234 } 1235 }, aResultCb); 1236 }, 1237 1238 getAlarms: function getAlarms(aNetworkId, aManifestURL, aResultCb) { 1239 let self = this; 1240 this.dbNewTxn(ALARMS_STORE_NAME, "readonly", function(txn, store) { 1241 if (DEBUG) { 1242 debug("Get alarms for " + aManifestURL); 1243 } 1244 1245 txn.result = []; 1246 store.index("manifestURL").openCursor(aManifestURL) 1247 .onsuccess = function onsuccess(event) { 1248 let cursor = event.target.result; 1249 if (!cursor) { 1250 return; 1251 } 1252 1253 if (!aNetworkId || cursor.value.networkId == aNetworkId) { 1254 txn.result.push(self.recordToAlarm(cursor.value)); 1255 } 1256 1257 cursor.continue(); 1258 } 1259 }, aResultCb); 1260 }, 1261 1262 _resetAlarms: function _resetAlarms(aNetworkId, aResultCb) { 1263 this.dbNewTxn(ALARMS_STORE_NAME, "readwrite", function(txn, store) { 1264 if (DEBUG) { 1265 debug("Reset alarms for network " + aNetworkId); 1266 } 1267 1268 let lowerFilter = [aNetworkId, 0]; 1269 let upperFilter = [aNetworkId, ""]; 1270 let range = IDBKeyRange.bound(lowerFilter, upperFilter); 1271 1272 store.index("alarm").openCursor(range).onsuccess = function onsuccess(event) { 1273 let cursor = event.target.result; 1274 if (cursor) { 1275 if (cursor.value.startTime) { 1276 cursor.value.relativeThreshold = cursor.value.threshold; 1277 cursor.update(cursor.value); 1278 } 1279 cursor.continue(); 1280 return; 1281 } 1282 }; 1283 }, aResultCb); 1284 } 1285}; 1286