summaryrefslogtreecommitdiff
path: root/dom/network/NetworkStatsDB.jsm
diff options
context:
space:
mode:
authorMatt A. Tobin <mattatobin@localhost.localdomain>2018-02-02 04:16:08 -0500
committerMatt A. Tobin <mattatobin@localhost.localdomain>2018-02-02 04:16:08 -0500
commit5f8de423f190bbb79a62f804151bc24824fa32d8 (patch)
tree10027f336435511475e392454359edea8e25895d /dom/network/NetworkStatsDB.jsm
parent49ee0794b5d912db1f95dce6eb52d781dc210db5 (diff)
downloaduxp-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.gz
Add m-esr52 at 52.6.0
Diffstat (limited to 'dom/network/NetworkStatsDB.jsm')
-rw-r--r--dom/network/NetworkStatsDB.jsm1285
1 files changed, 1285 insertions, 0 deletions
diff --git a/dom/network/NetworkStatsDB.jsm b/dom/network/NetworkStatsDB.jsm
new file mode 100644
index 0000000000..aa74d40ad9
--- /dev/null
+++ b/dom/network/NetworkStatsDB.jsm
@@ -0,0 +1,1285 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = ['NetworkStatsDB'];
+
+const DEBUG = false;
+function debug(s) { dump("-*- NetworkStatsDB: " + s + "\n"); }
+
+const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/IndexedDBHelper.jsm");
+Cu.importGlobalProperties(["indexedDB"]);
+
+XPCOMUtils.defineLazyServiceGetter(this, "appsService",
+ "@mozilla.org/AppsService;1",
+ "nsIAppsService");
+
+const DB_NAME = "net_stats";
+const DB_VERSION = 9;
+const DEPRECATED_STATS_STORE_NAME =
+ [
+ "net_stats_v2", // existed only in DB version 2
+ "net_stats", // existed in DB version 1 and 3 to 5
+ "net_stats_store", // existed in DB version 6 to 8
+ ];
+const STATS_STORE_NAME = "net_stats_store_v3"; // since DB version 9
+const ALARMS_STORE_NAME = "net_alarm";
+
+// Constant defining the maximum values allowed per interface. If more, older
+// will be erased.
+const VALUES_MAX_LENGTH = 6 * 30;
+
+// Constant defining the rate of the samples. Daily.
+const SAMPLE_RATE = 1000 * 60 * 60 * 24;
+
+this.NetworkStatsDB = function NetworkStatsDB() {
+ if (DEBUG) {
+ debug("Constructor");
+ }
+ this.initDBHelper(DB_NAME, DB_VERSION, [STATS_STORE_NAME, ALARMS_STORE_NAME]);
+}
+
+NetworkStatsDB.prototype = {
+ __proto__: IndexedDBHelper.prototype,
+
+ dbNewTxn: function dbNewTxn(store_name, txn_type, callback, txnCb) {
+ function successCb(result) {
+ txnCb(null, result);
+ }
+ function errorCb(error) {
+ txnCb(error, null);
+ }
+ return this.newTxn(txn_type, store_name, callback, successCb, errorCb);
+ },
+
+ /**
+ * The onupgradeneeded handler of the IDBOpenDBRequest.
+ * This function is called in IndexedDBHelper open() method.
+ *
+ * @param {IDBTransaction} aTransaction
+ * {IDBDatabase} aDb
+ * {64-bit integer} aOldVersion The version number on local storage.
+ * {64-bit integer} aNewVersion The version number to be upgraded to.
+ *
+ * @note Be careful with the database upgrade pattern.
+ * Because IndexedDB operations are performed asynchronously, we must
+ * apply a recursive approach instead of an iterative approach while
+ * upgrading versions.
+ */
+ upgradeSchema: function upgradeSchema(aTransaction, aDb, aOldVersion, aNewVersion) {
+ if (DEBUG) {
+ debug("upgrade schema from: " + aOldVersion + " to " + aNewVersion + " called!");
+ }
+ let db = aDb;
+ let objectStore;
+
+ // An array of upgrade functions for each version.
+ let upgradeSteps = [
+ function upgrade0to1() {
+ if (DEBUG) debug("Upgrade 0 to 1: Create object stores and indexes.");
+
+ // Create the initial database schema.
+ objectStore = db.createObjectStore(DEPRECATED_STATS_STORE_NAME[1],
+ { keyPath: ["connectionType", "timestamp"] });
+ objectStore.createIndex("connectionType", "connectionType", { unique: false });
+ objectStore.createIndex("timestamp", "timestamp", { unique: false });
+ objectStore.createIndex("rxBytes", "rxBytes", { unique: false });
+ objectStore.createIndex("txBytes", "txBytes", { unique: false });
+ objectStore.createIndex("rxTotalBytes", "rxTotalBytes", { unique: false });
+ objectStore.createIndex("txTotalBytes", "txTotalBytes", { unique: false });
+
+ upgradeNextVersion();
+ },
+
+ function upgrade1to2() {
+ if (DEBUG) debug("Upgrade 1 to 2: Do nothing.");
+ upgradeNextVersion();
+ },
+
+ function upgrade2to3() {
+ if (DEBUG) debug("Upgrade 2 to 3: Add keyPath appId to object store.");
+
+ // In order to support per-app traffic data storage, the original
+ // objectStore needs to be replaced by a new objectStore with new
+ // key path ("appId") and new index ("appId").
+ // Also, since now networks are identified by their
+ // [networkId, networkType] not just by their connectionType,
+ // to modify the keyPath is mandatory to delete the object store
+ // and create it again. Old data is going to be deleted because the
+ // networkId for each sample can not be set.
+
+ // In version 1.2 objectStore name was 'net_stats_v2', to avoid errors when
+ // upgrading from 1.2 to 1.3 objectStore name should be checked.
+ let stores = db.objectStoreNames;
+ let deprecatedName = DEPRECATED_STATS_STORE_NAME[0];
+ let storeName = DEPRECATED_STATS_STORE_NAME[1];
+ if(stores.contains(deprecatedName)) {
+ // Delete the obsolete stats store.
+ db.deleteObjectStore(deprecatedName);
+ } else {
+ // Re-create stats object store without copying records.
+ db.deleteObjectStore(storeName);
+ }
+
+ objectStore = db.createObjectStore(storeName, { keyPath: ["appId", "network", "timestamp"] });
+ objectStore.createIndex("appId", "appId", { unique: false });
+ objectStore.createIndex("network", "network", { unique: false });
+ objectStore.createIndex("networkType", "networkType", { unique: false });
+ objectStore.createIndex("timestamp", "timestamp", { unique: false });
+ objectStore.createIndex("rxBytes", "rxBytes", { unique: false });
+ objectStore.createIndex("txBytes", "txBytes", { unique: false });
+ objectStore.createIndex("rxTotalBytes", "rxTotalBytes", { unique: false });
+ objectStore.createIndex("txTotalBytes", "txTotalBytes", { unique: false });
+
+ upgradeNextVersion();
+ },
+
+ function upgrade3to4() {
+ if (DEBUG) debug("Upgrade 3 to 4: Delete redundant indexes.");
+
+ // Delete redundant indexes (leave "network" only).
+ objectStore = aTransaction.objectStore(DEPRECATED_STATS_STORE_NAME[1]);
+ if (objectStore.indexNames.contains("appId")) {
+ objectStore.deleteIndex("appId");
+ }
+ if (objectStore.indexNames.contains("networkType")) {
+ objectStore.deleteIndex("networkType");
+ }
+ if (objectStore.indexNames.contains("timestamp")) {
+ objectStore.deleteIndex("timestamp");
+ }
+ if (objectStore.indexNames.contains("rxBytes")) {
+ objectStore.deleteIndex("rxBytes");
+ }
+ if (objectStore.indexNames.contains("txBytes")) {
+ objectStore.deleteIndex("txBytes");
+ }
+ if (objectStore.indexNames.contains("rxTotalBytes")) {
+ objectStore.deleteIndex("rxTotalBytes");
+ }
+ if (objectStore.indexNames.contains("txTotalBytes")) {
+ objectStore.deleteIndex("txTotalBytes");
+ }
+
+ upgradeNextVersion();
+ },
+
+ function upgrade4to5() {
+ if (DEBUG) debug("Upgrade 4 to 5: Create object store for alarms.");
+
+ // In order to manage alarms, it is necessary to use a global counter
+ // (totalBytes) that will increase regardless of the system reboot.
+ objectStore = aTransaction.objectStore(DEPRECATED_STATS_STORE_NAME[1]);
+
+ // Now, systemBytes will hold the old totalBytes and totalBytes will
+ // keep the increasing counter. |counters| will keep the track of
+ // accumulated values.
+ let counters = {};
+
+ objectStore.openCursor().onsuccess = function(event) {
+ let cursor = event.target.result;
+ if (!cursor){
+ // upgrade4to5 completed now.
+ upgradeNextVersion();
+ return;
+ }
+
+ cursor.value.rxSystemBytes = cursor.value.rxTotalBytes;
+ cursor.value.txSystemBytes = cursor.value.txTotalBytes;
+
+ if (cursor.value.appId == 0) {
+ let netId = cursor.value.network[0] + '' + cursor.value.network[1];
+ if (!counters[netId]) {
+ counters[netId] = {
+ rxCounter: 0,
+ txCounter: 0,
+ lastRx: 0,
+ lastTx: 0
+ };
+ }
+
+ let rxDiff = cursor.value.rxSystemBytes - counters[netId].lastRx;
+ let txDiff = cursor.value.txSystemBytes - counters[netId].lastTx;
+ if (rxDiff < 0 || txDiff < 0) {
+ // System reboot between samples, so take the current one.
+ rxDiff = cursor.value.rxSystemBytes;
+ txDiff = cursor.value.txSystemBytes;
+ }
+
+ counters[netId].rxCounter += rxDiff;
+ counters[netId].txCounter += txDiff;
+ cursor.value.rxTotalBytes = counters[netId].rxCounter;
+ cursor.value.txTotalBytes = counters[netId].txCounter;
+
+ counters[netId].lastRx = cursor.value.rxSystemBytes;
+ counters[netId].lastTx = cursor.value.txSystemBytes;
+ } else {
+ cursor.value.rxTotalBytes = cursor.value.rxSystemBytes;
+ cursor.value.txTotalBytes = cursor.value.txSystemBytes;
+ }
+
+ cursor.update(cursor.value);
+ cursor.continue();
+ };
+
+ // Create object store for alarms.
+ objectStore = db.createObjectStore(ALARMS_STORE_NAME, { keyPath: "id", autoIncrement: true });
+ objectStore.createIndex("alarm", ['networkId','threshold'], { unique: false });
+ objectStore.createIndex("manifestURL", "manifestURL", { unique: false });
+ },
+
+ function upgrade5to6() {
+ if (DEBUG) debug("Upgrade 5 to 6: Add keyPath serviceType to object store.");
+
+ // In contrast to "per-app" traffic data, "system-only" traffic data
+ // refers to data which can not be identified by any applications.
+ // To further support "system-only" data storage, the data can be
+ // saved by service type (e.g., Tethering, OTA). Thus it's needed to
+ // have a new key ("serviceType") for the ojectStore.
+ let newObjectStore;
+ let deprecatedName = DEPRECATED_STATS_STORE_NAME[1];
+ newObjectStore = db.createObjectStore(DEPRECATED_STATS_STORE_NAME[2],
+ { keyPath: ["appId", "serviceType", "network", "timestamp"] });
+ newObjectStore.createIndex("network", "network", { unique: false });
+
+ // Copy the data from the original objectStore to the new objectStore.
+ objectStore = aTransaction.objectStore(deprecatedName);
+ objectStore.openCursor().onsuccess = function(event) {
+ let cursor = event.target.result;
+ if (!cursor) {
+ db.deleteObjectStore(deprecatedName);
+ // upgrade5to6 completed now.
+ upgradeNextVersion();
+ return;
+ }
+
+ let newStats = cursor.value;
+ newStats.serviceType = "";
+ newObjectStore.put(newStats);
+ cursor.continue();
+ };
+ },
+
+ function upgrade6to7() {
+ if (DEBUG) debug("Upgrade 6 to 7: Replace alarm threshold by relativeThreshold.");
+
+ // Replace threshold attribute of alarm index by relativeThreshold in alarms DB.
+ // Now alarms are indexed by relativeThreshold, which is the threshold relative
+ // to current system stats.
+ let alarmsStore = aTransaction.objectStore(ALARMS_STORE_NAME);
+
+ // Delete "alarm" index.
+ if (alarmsStore.indexNames.contains("alarm")) {
+ alarmsStore.deleteIndex("alarm");
+ }
+
+ // Create new "alarm" index.
+ alarmsStore.createIndex("alarm", ['networkId','relativeThreshold'], { unique: false });
+
+ // Populate new "alarm" index attributes.
+ alarmsStore.openCursor().onsuccess = function(event) {
+ let cursor = event.target.result;
+ if (!cursor) {
+ upgrade6to7_updateTotalBytes();
+ return;
+ }
+
+ cursor.value.relativeThreshold = cursor.value.threshold;
+ cursor.value.absoluteThreshold = cursor.value.threshold;
+ delete cursor.value.threshold;
+
+ cursor.update(cursor.value);
+ cursor.continue();
+ }
+
+ function upgrade6to7_updateTotalBytes() {
+ if (DEBUG) debug("Upgrade 6 to 7: Update TotalBytes.");
+ // Previous versions save accumulative totalBytes, increasing although the system
+ // reboots or resets stats. But is necessary to reset the total counters when reset
+ // through 'clearInterfaceStats'.
+ let statsStore = aTransaction.objectStore(DEPRECATED_STATS_STORE_NAME[2]);
+ let networks = [];
+
+ // Find networks stored in the database.
+ statsStore.index("network").openKeyCursor(null, "nextunique").onsuccess = function(event) {
+ let cursor = event.target.result;
+
+ // Store each network into an array.
+ if (cursor) {
+ networks.push(cursor.key);
+ cursor.continue();
+ return;
+ }
+
+ // Start to deal with each network.
+ let pending = networks.length;
+
+ if (pending === 0) {
+ // Found no records of network. upgrade6to7 completed now.
+ upgradeNextVersion();
+ return;
+ }
+
+ networks.forEach(function(network) {
+ let lowerFilter = [0, "", network, 0];
+ let upperFilter = [0, "", network, ""];
+ let range = IDBKeyRange.bound(lowerFilter, upperFilter, false, false);
+
+ // Find number of samples for a given network.
+ statsStore.count(range).onsuccess = function(event) {
+ let recordCount = event.target.result;
+
+ // If there are more samples than the max allowed, there is no way to know
+ // when does reset take place.
+ if (recordCount === 0 || recordCount >= VALUES_MAX_LENGTH) {
+ pending--;
+ if (pending === 0) {
+ upgradeNextVersion();
+ }
+ return;
+ }
+
+ let last = null;
+ // Reset detected if the first sample totalCounters are different than bytes
+ // counters. If so, the total counters should be recalculated.
+ statsStore.openCursor(range).onsuccess = function(event) {
+ let cursor = event.target.result;
+ if (!cursor) {
+ pending--;
+ if (pending === 0) {
+ upgradeNextVersion();
+ }
+ return;
+ }
+ if (!last) {
+ if (cursor.value.rxTotalBytes == cursor.value.rxBytes &&
+ cursor.value.txTotalBytes == cursor.value.txBytes) {
+ pending--;
+ if (pending === 0) {
+ upgradeNextVersion();
+ }
+ return;
+ }
+
+ cursor.value.rxTotalBytes = cursor.value.rxBytes;
+ cursor.value.txTotalBytes = cursor.value.txBytes;
+ cursor.update(cursor.value);
+ last = cursor.value;
+ cursor.continue();
+ return;
+ }
+
+ // Recalculate the total counter for last / current sample
+ cursor.value.rxTotalBytes = last.rxTotalBytes + cursor.value.rxBytes;
+ cursor.value.txTotalBytes = last.txTotalBytes + cursor.value.txBytes;
+ cursor.update(cursor.value);
+ last = cursor.value;
+ cursor.continue();
+ }
+ }
+ }, this); // end of networks.forEach()
+ }; // end of statsStore.index("network").openKeyCursor().onsuccess callback
+ } // end of function upgrade6to7_updateTotalBytes
+ },
+
+ function upgrade7to8() {
+ if (DEBUG) debug("Upgrade 7 to 8: Create index serviceType.");
+
+ // Create index for 'ServiceType' in order to make it retrievable.
+ let statsStore = aTransaction.objectStore(DEPRECATED_STATS_STORE_NAME[2]);
+ statsStore.createIndex("serviceType", "serviceType", { unique: false });
+
+ upgradeNextVersion();
+ },
+
+ function upgrade8to9() {
+ if (DEBUG) debug("Upgrade 8 to 9: Add keyPath isInBrowser to " +
+ "network stats object store");
+
+ // Since B2G v2.0, there is no stand-alone browser app anymore.
+ // The browser app is a mozbrowser iframe element owned by system app.
+ // In order to separate traffic generated from system and browser, we
+ // have to add a new attribute |isInBrowser| as keyPath.
+ // Refer to bug 1070944 for more detail.
+ let newObjectStore;
+ let deprecatedName = DEPRECATED_STATS_STORE_NAME[2];
+ newObjectStore = db.createObjectStore(STATS_STORE_NAME,
+ { keyPath: ["appId", "isInBrowser", "serviceType",
+ "network", "timestamp"] });
+ newObjectStore.createIndex("network", "network", { unique: false });
+ newObjectStore.createIndex("serviceType", "serviceType", { unique: false });
+
+ // Copy records from the current object store to the new one.
+ objectStore = aTransaction.objectStore(deprecatedName);
+ objectStore.openCursor().onsuccess = function (event) {
+ let cursor = event.target.result;
+ if (!cursor) {
+ db.deleteObjectStore(deprecatedName);
+ // upgrade8to9 completed now.
+ return;
+ }
+ let newStats = cursor.value;
+ // Augment records by adding the new isInBrowser attribute.
+ // Notes:
+ // 1. Key value cannot be boolean type. Use 1/0 instead of true/false.
+ // 2. Most traffic of system app should come from its browser iframe,
+ // thus assign isInBrowser as 1 for system app.
+ let manifestURL = appsService.getManifestURLByLocalId(newStats.appId);
+ if (manifestURL && manifestURL.search(/app:\/\/system\./) === 0) {
+ newStats.isInBrowser = 1;
+ } else {
+ newStats.isInBrowser = 0;
+ }
+ newObjectStore.put(newStats);
+ cursor.continue();
+ };
+ }
+ ];
+
+ let index = aOldVersion;
+ let outer = this;
+
+ function upgradeNextVersion() {
+ if (index == aNewVersion) {
+ debug("Upgrade finished.");
+ return;
+ }
+
+ try {
+ var i = index++;
+ if (DEBUG) debug("Upgrade step: " + i + "\n");
+ upgradeSteps[i].call(outer);
+ } catch (ex) {
+ dump("Caught exception " + ex);
+ throw ex;
+ return;
+ }
+ }
+
+ if (aNewVersion > upgradeSteps.length) {
+ debug("No migration steps for the new version!");
+ aTransaction.abort();
+ return;
+ }
+
+ upgradeNextVersion();
+ },
+
+ importData: function importData(aStats) {
+ let stats = { appId: aStats.appId,
+ isInBrowser: aStats.isInBrowser ? 1 : 0,
+ serviceType: aStats.serviceType,
+ network: [aStats.networkId, aStats.networkType],
+ timestamp: aStats.timestamp,
+ rxBytes: aStats.rxBytes,
+ txBytes: aStats.txBytes,
+ rxSystemBytes: aStats.rxSystemBytes,
+ txSystemBytes: aStats.txSystemBytes,
+ rxTotalBytes: aStats.rxTotalBytes,
+ txTotalBytes: aStats.txTotalBytes };
+
+ return stats;
+ },
+
+ exportData: function exportData(aStats) {
+ let stats = { appId: aStats.appId,
+ isInBrowser: aStats.isInBrowser ? true : false,
+ serviceType: aStats.serviceType,
+ networkId: aStats.network[0],
+ networkType: aStats.network[1],
+ timestamp: aStats.timestamp,
+ rxBytes: aStats.rxBytes,
+ txBytes: aStats.txBytes,
+ rxTotalBytes: aStats.rxTotalBytes,
+ txTotalBytes: aStats.txTotalBytes };
+
+ return stats;
+ },
+
+ normalizeDate: function normalizeDate(aDate) {
+ // Convert to UTC according to timezone and
+ // filter timestamp to get SAMPLE_RATE precission
+ let timestamp = aDate.getTime() - aDate.getTimezoneOffset() * 60 * 1000;
+ timestamp = Math.floor(timestamp / SAMPLE_RATE) * SAMPLE_RATE;
+ return timestamp;
+ },
+
+ saveStats: function saveStats(aStats, aResultCb) {
+ let isAccumulative = aStats.isAccumulative;
+ let timestamp = this.normalizeDate(aStats.date);
+
+ let stats = { appId: aStats.appId,
+ isInBrowser: aStats.isInBrowser,
+ serviceType: aStats.serviceType,
+ networkId: aStats.networkId,
+ networkType: aStats.networkType,
+ timestamp: timestamp,
+ rxBytes: isAccumulative ? 0 : aStats.rxBytes,
+ txBytes: isAccumulative ? 0 : aStats.txBytes,
+ rxSystemBytes: isAccumulative ? aStats.rxBytes : 0,
+ txSystemBytes: isAccumulative ? aStats.txBytes : 0,
+ rxTotalBytes: isAccumulative ? aStats.rxBytes : 0,
+ txTotalBytes: isAccumulative ? aStats.txBytes : 0 };
+
+ stats = this.importData(stats);
+
+ this.dbNewTxn(STATS_STORE_NAME, "readwrite", function(aTxn, aStore) {
+ if (DEBUG) {
+ debug("Filtered time: " + new Date(timestamp));
+ debug("New stats: " + JSON.stringify(stats));
+ }
+
+ let lowerFilter = [stats.appId, stats.isInBrowser, stats.serviceType,
+ stats.network, 0];
+ let upperFilter = [stats.appId, stats.isInBrowser, stats.serviceType,
+ stats.network, ""];
+ let range = IDBKeyRange.bound(lowerFilter, upperFilter, false, false);
+
+ let request = aStore.openCursor(range, 'prev');
+ request.onsuccess = function onsuccess(event) {
+ let cursor = event.target.result;
+ if (!cursor) {
+ // Empty, so save first element.
+
+ if (!isAccumulative) {
+ this._saveStats(aTxn, aStore, stats);
+ return;
+ }
+
+ // There could be a time delay between the point when the network
+ // interface comes up and the point when the database is initialized.
+ // In this short interval some traffic data are generated but are not
+ // registered by the first sample.
+ stats.rxBytes = stats.rxTotalBytes;
+ stats.txBytes = stats.txTotalBytes;
+
+ // However, if the interface is not switched on after the database is
+ // initialized (dual sim use case) stats should be set to 0.
+ let req = aStore.index("network").openKeyCursor(null, "nextunique");
+ req.onsuccess = function onsuccess(event) {
+ let cursor = event.target.result;
+ if (cursor) {
+ if (cursor.key[1] == stats.network[1]) {
+ stats.rxBytes = 0;
+ stats.txBytes = 0;
+ this._saveStats(aTxn, aStore, stats);
+ return;
+ }
+
+ cursor.continue();
+ return;
+ }
+
+ this._saveStats(aTxn, aStore, stats);
+ }.bind(this);
+
+ return;
+ }
+
+ // There are old samples
+ if (DEBUG) {
+ debug("Last value " + JSON.stringify(cursor.value));
+ }
+
+ // Remove stats previous to now - VALUE_MAX_LENGTH
+ this._removeOldStats(aTxn, aStore, stats.appId, stats.isInBrowser,
+ stats.serviceType, stats.network, stats.timestamp);
+
+ // Process stats before save
+ this._processSamplesDiff(aTxn, aStore, cursor, stats, isAccumulative);
+ }.bind(this);
+ }.bind(this), aResultCb);
+ },
+
+ /*
+ * This function check that stats are saved in the database following the sample rate.
+ * In this way is easier to find elements when stats are requested.
+ */
+ _processSamplesDiff: function _processSamplesDiff(aTxn,
+ aStore,
+ aLastSampleCursor,
+ aNewSample,
+ aIsAccumulative) {
+ let lastSample = aLastSampleCursor.value;
+
+ // Get difference between last and new sample.
+ let diff = (aNewSample.timestamp - lastSample.timestamp) / SAMPLE_RATE;
+ if (diff % 1) {
+ // diff is decimal, so some error happened because samples are stored as a multiple
+ // of SAMPLE_RATE
+ aTxn.abort();
+ throw new Error("Error processing samples");
+ }
+
+ if (DEBUG) {
+ debug("New: " + aNewSample.timestamp + " - Last: " +
+ lastSample.timestamp + " - diff: " + diff);
+ }
+
+ // If the incoming data has a accumulation feature, the new
+ // |txBytes|/|rxBytes| is assigend by differnces between the new
+ // |txTotalBytes|/|rxTotalBytes| and the last |txTotalBytes|/|rxTotalBytes|.
+ // Else, if incoming data is non-accumulative, the |txBytes|/|rxBytes|
+ // is the new |txBytes|/|rxBytes|.
+ let rxDiff = 0;
+ let txDiff = 0;
+ if (aIsAccumulative) {
+ rxDiff = aNewSample.rxSystemBytes - lastSample.rxSystemBytes;
+ txDiff = aNewSample.txSystemBytes - lastSample.txSystemBytes;
+ if (rxDiff < 0 || txDiff < 0) {
+ rxDiff = aNewSample.rxSystemBytes;
+ txDiff = aNewSample.txSystemBytes;
+ }
+ aNewSample.rxBytes = rxDiff;
+ aNewSample.txBytes = txDiff;
+
+ aNewSample.rxTotalBytes = lastSample.rxTotalBytes + rxDiff;
+ aNewSample.txTotalBytes = lastSample.txTotalBytes + txDiff;
+ } else {
+ rxDiff = aNewSample.rxBytes;
+ txDiff = aNewSample.txBytes;
+ }
+
+ if (diff == 1) {
+ // New element.
+
+ // If the incoming data is non-accumulative, the new
+ // |rxTotalBytes|/|txTotalBytes| needs to be updated by adding new
+ // |rxBytes|/|txBytes| to the last |rxTotalBytes|/|txTotalBytes|.
+ if (!aIsAccumulative) {
+ aNewSample.rxTotalBytes = aNewSample.rxBytes + lastSample.rxTotalBytes;
+ aNewSample.txTotalBytes = aNewSample.txBytes + lastSample.txTotalBytes;
+ }
+
+ this._saveStats(aTxn, aStore, aNewSample);
+ return;
+ }
+ if (diff > 1) {
+ // Some samples lost. Device off during one or more samplerate periods.
+ // Time or timezone changed
+ // Add lost samples with 0 bytes and the actual one.
+ if (diff > VALUES_MAX_LENGTH) {
+ diff = VALUES_MAX_LENGTH;
+ }
+
+ let data = [];
+ for (let i = diff - 2; i >= 0; i--) {
+ let time = aNewSample.timestamp - SAMPLE_RATE * (i + 1);
+ let sample = { appId: aNewSample.appId,
+ isInBrowser: aNewSample.isInBrowser,
+ serviceType: aNewSample.serviceType,
+ network: aNewSample.network,
+ timestamp: time,
+ rxBytes: 0,
+ txBytes: 0,
+ rxSystemBytes: lastSample.rxSystemBytes,
+ txSystemBytes: lastSample.txSystemBytes,
+ rxTotalBytes: lastSample.rxTotalBytes,
+ txTotalBytes: lastSample.txTotalBytes };
+
+ data.push(sample);
+ }
+
+ data.push(aNewSample);
+ this._saveStats(aTxn, aStore, data);
+ return;
+ }
+ if (diff == 0 || diff < 0) {
+ // New element received before samplerate period. It means that device has
+ // been restarted (or clock / timezone change).
+ // Update element. If diff < 0, clock or timezone changed back. Place data
+ // in the last sample.
+
+ // Old |rxTotalBytes|/|txTotalBytes| needs to get updated by adding the
+ // last |rxTotalBytes|/|txTotalBytes|.
+ lastSample.rxBytes += rxDiff;
+ lastSample.txBytes += txDiff;
+ lastSample.rxSystemBytes = aNewSample.rxSystemBytes;
+ lastSample.txSystemBytes = aNewSample.txSystemBytes;
+ lastSample.rxTotalBytes += rxDiff;
+ lastSample.txTotalBytes += txDiff;
+
+ if (DEBUG) {
+ debug("Update: " + JSON.stringify(lastSample));
+ }
+ let req = aLastSampleCursor.update(lastSample);
+ }
+ },
+
+ _saveStats: function _saveStats(aTxn, aStore, aNetworkStats) {
+ if (DEBUG) {
+ debug("_saveStats: " + JSON.stringify(aNetworkStats));
+ }
+
+ if (Array.isArray(aNetworkStats)) {
+ let len = aNetworkStats.length - 1;
+ for (let i = 0; i <= len; i++) {
+ aStore.put(aNetworkStats[i]);
+ }
+ } else {
+ aStore.put(aNetworkStats);
+ }
+ },
+
+ _removeOldStats: function _removeOldStats(aTxn, aStore, aAppId, aIsInBrowser,
+ aServiceType, aNetwork, aDate) {
+ // Callback function to remove old items when new ones are added.
+ let filterDate = aDate - (SAMPLE_RATE * VALUES_MAX_LENGTH - 1);
+ let lowerFilter = [aAppId, aIsInBrowser, aServiceType, aNetwork, 0];
+ let upperFilter = [aAppId, aIsInBrowser, aServiceType, aNetwork, filterDate];
+ let range = IDBKeyRange.bound(lowerFilter, upperFilter, false, false);
+ let lastSample = null;
+ let self = this;
+
+ aStore.openCursor(range).onsuccess = function(event) {
+ var cursor = event.target.result;
+ if (cursor) {
+ lastSample = cursor.value;
+ cursor.delete();
+ cursor.continue();
+ return;
+ }
+
+ // If all samples for a network are removed, an empty sample
+ // has to be saved to keep the totalBytes in order to compute
+ // future samples because system counters are not set to 0.
+ // Thus, if there are no samples left, the last sample removed
+ // will be saved again after setting its bytes to 0.
+ let request = aStore.index("network").openCursor(aNetwork);
+ request.onsuccess = function onsuccess(event) {
+ let cursor = event.target.result;
+ if (!cursor && lastSample != null) {
+ let timestamp = new Date();
+ timestamp = self.normalizeDate(timestamp);
+ lastSample.timestamp = timestamp;
+ lastSample.rxBytes = 0;
+ lastSample.txBytes = 0;
+ self._saveStats(aTxn, aStore, lastSample);
+ }
+ };
+ };
+ },
+
+ clearInterfaceStats: function clearInterfaceStats(aNetwork, aResultCb) {
+ let network = [aNetwork.network.id, aNetwork.network.type];
+ let self = this;
+
+ // Clear and save an empty sample to keep sync with system counters
+ this.dbNewTxn(STATS_STORE_NAME, "readwrite", function(aTxn, aStore) {
+ let sample = null;
+ let request = aStore.index("network").openCursor(network, "prev");
+ request.onsuccess = function onsuccess(event) {
+ let cursor = event.target.result;
+ if (cursor) {
+ if (!sample && cursor.value.appId == 0) {
+ sample = cursor.value;
+ }
+
+ cursor.delete();
+ cursor.continue();
+ return;
+ }
+
+ if (sample) {
+ let timestamp = new Date();
+ timestamp = self.normalizeDate(timestamp);
+ sample.timestamp = timestamp;
+ sample.appId = 0;
+ sample.isInBrowser = 0;
+ sample.serviceType = "";
+ sample.rxBytes = 0;
+ sample.txBytes = 0;
+ sample.rxTotalBytes = 0;
+ sample.txTotalBytes = 0;
+
+ self._saveStats(aTxn, aStore, sample);
+ }
+ };
+ }, this._resetAlarms.bind(this, aNetwork.networkId, aResultCb));
+ },
+
+ clearStats: function clearStats(aNetworks, aResultCb) {
+ let index = 0;
+ let stats = [];
+ let self = this;
+
+ let callback = function(aError, aResult) {
+ index++;
+
+ if (!aError && index < aNetworks.length) {
+ self.clearInterfaceStats(aNetworks[index], callback);
+ return;
+ }
+
+ aResultCb(aError, aResult);
+ };
+
+ if (!aNetworks[index]) {
+ aResultCb(null, true);
+ return;
+ }
+ this.clearInterfaceStats(aNetworks[index], callback);
+ },
+
+ getCurrentStats: function getCurrentStats(aNetwork, aDate, aResultCb) {
+ if (DEBUG) {
+ debug("Get current stats for " + JSON.stringify(aNetwork) + " since " + aDate);
+ }
+
+ let network = [aNetwork.id, aNetwork.type];
+ if (aDate) {
+ this._getCurrentStatsFromDate(network, aDate, aResultCb);
+ return;
+ }
+
+ this._getCurrentStats(network, aResultCb);
+ },
+
+ _getCurrentStats: function _getCurrentStats(aNetwork, aResultCb) {
+ this.dbNewTxn(STATS_STORE_NAME, "readonly", function(txn, store) {
+ let request = null;
+ let upperFilter = [0, 1, "", aNetwork, Date.now()];
+ let range = IDBKeyRange.upperBound(upperFilter, false);
+ let result = { rxBytes: 0, txBytes: 0,
+ rxTotalBytes: 0, txTotalBytes: 0 };
+
+ request = store.openCursor(range, "prev");
+
+ request.onsuccess = function onsuccess(event) {
+ let cursor = event.target.result;
+ if (cursor) {
+ result.rxBytes = result.rxTotalBytes = cursor.value.rxTotalBytes;
+ result.txBytes = result.txTotalBytes = cursor.value.txTotalBytes;
+ }
+
+ txn.result = result;
+ };
+ }.bind(this), aResultCb);
+ },
+
+ _getCurrentStatsFromDate: function _getCurrentStatsFromDate(aNetwork, aDate, aResultCb) {
+ aDate = new Date(aDate);
+ this.dbNewTxn(STATS_STORE_NAME, "readonly", function(txn, store) {
+ let request = null;
+ let start = this.normalizeDate(aDate);
+ let upperFilter = [0, 1, "", aNetwork, Date.now()];
+ let range = IDBKeyRange.upperBound(upperFilter, false);
+ let result = { rxBytes: 0, txBytes: 0,
+ rxTotalBytes: 0, txTotalBytes: 0 };
+
+ request = store.openCursor(range, "prev");
+
+ request.onsuccess = function onsuccess(event) {
+ let cursor = event.target.result;
+ if (cursor) {
+ result.rxBytes = result.rxTotalBytes = cursor.value.rxTotalBytes;
+ result.txBytes = result.txTotalBytes = cursor.value.txTotalBytes;
+ }
+
+ let timestamp = cursor.value.timestamp;
+ let range = IDBKeyRange.lowerBound(lowerFilter, false);
+ request = store.openCursor(range);
+
+ request.onsuccess = function onsuccess(event) {
+ let cursor = event.target.result;
+ if (cursor) {
+ if (cursor.value.timestamp == timestamp) {
+ // There is one sample only.
+ result.rxBytes = cursor.value.rxBytes;
+ result.txBytes = cursor.value.txBytes;
+ } else {
+ result.rxBytes -= cursor.value.rxTotalBytes;
+ result.txBytes -= cursor.value.txTotalBytes;
+ }
+ }
+
+ txn.result = result;
+ };
+ };
+ }.bind(this), aResultCb);
+ },
+
+ find: function find(aResultCb, aAppId, aBrowsingTrafficOnly, aServiceType,
+ aNetwork, aStart, aEnd, aAppManifestURL) {
+ let offset = (new Date()).getTimezoneOffset() * 60 * 1000;
+ let start = this.normalizeDate(aStart);
+ let end = this.normalizeDate(aEnd);
+
+ if (DEBUG) {
+ debug("Find samples for appId: " + aAppId +
+ " browsingTrafficOnly: " + aBrowsingTrafficOnly +
+ " serviceType: " + aServiceType +
+ " network: " + JSON.stringify(aNetwork) + " from " + start +
+ " until " + end);
+ debug("Start time: " + new Date(start));
+ debug("End time: " + new Date(end));
+ }
+
+ // Find samples of browsing traffic (isInBrowser = 1) first since they are
+ // needed no matter browsingTrafficOnly is true or false.
+ // We have to make two queries to database because we cannot filter correct
+ // records by a single query that sets ranges for two keys (isInBrowser and
+ // timestamp). We think it is because the keyPath contains an array
+ // (network) so such query does not work.
+ this.dbNewTxn(STATS_STORE_NAME, "readonly", function(aTxn, aStore) {
+ let network = [aNetwork.id, aNetwork.type];
+ let lowerFilter = [aAppId, 1, aServiceType, network, start];
+ let upperFilter = [aAppId, 1, aServiceType, network, end];
+ let range = IDBKeyRange.bound(lowerFilter, upperFilter, false, false);
+
+ let data = [];
+
+ if (!aTxn.result) {
+ aTxn.result = {};
+ }
+ aTxn.result.appManifestURL = aAppManifestURL;
+ aTxn.result.browsingTrafficOnly = aBrowsingTrafficOnly;
+ aTxn.result.serviceType = aServiceType;
+ aTxn.result.network = aNetwork;
+ aTxn.result.start = aStart;
+ aTxn.result.end = aEnd;
+
+ let request = aStore.openCursor(range).onsuccess = function(event) {
+ var cursor = event.target.result;
+ if (cursor){
+ // We use rxTotalBytes/txTotalBytes instead of rxBytes/txBytes for
+ // the first (oldest) sample. The rx/txTotalBytes fields record
+ // accumulative usage amount, which means even if old samples were
+ // expired and removed from the Database, we can still obtain the
+ // correct network usage.
+ if (data.length == 0) {
+ data.push({ rxBytes: cursor.value.rxTotalBytes,
+ txBytes: cursor.value.txTotalBytes,
+ date: new Date(cursor.value.timestamp + offset) });
+ } else {
+ data.push({ rxBytes: cursor.value.rxBytes,
+ txBytes: cursor.value.txBytes,
+ date: new Date(cursor.value.timestamp + offset) });
+ }
+ cursor.continue();
+ return;
+ }
+
+ if (aBrowsingTrafficOnly) {
+ this.fillResultSamples(start + offset, end + offset, data);
+ aTxn.result.data = data;
+ return;
+ }
+
+ // Find samples of app traffic (isInBrowser = 0) as well if
+ // browsingTrafficOnly is false.
+ lowerFilter = [aAppId, 0, aServiceType, network, start];
+ upperFilter = [aAppId, 0, aServiceType, network, end];
+ range = IDBKeyRange.bound(lowerFilter, upperFilter, false, false);
+ request = aStore.openCursor(range).onsuccess = function(event) {
+ cursor = event.target.result;
+ if (cursor) {
+ var date = new Date(cursor.value.timestamp + offset);
+ var foundData = data.find(function (element, index, array) {
+ if (element.date.getTime() !== date.getTime()) {
+ return false;
+ }
+ return element;
+ }, date);
+
+ if (foundData) {
+ foundData.rxBytes += cursor.value.rxBytes;
+ foundData.txBytes += cursor.value.txBytes;
+ } else {
+ // We use rxTotalBytes/txTotalBytes instead of rxBytes/txBytes
+ // for the first (oldest) sample. The rx/txTotalBytes fields
+ // record accumulative usage amount, which means even if old
+ // samples were expired and removed from the Database, we can
+ // still obtain the correct network usage.
+ if (data.length == 0) {
+ data.push({ rxBytes: cursor.value.rxTotalBytes,
+ txBytes: cursor.value.txTotalBytes,
+ date: new Date(cursor.value.timestamp + offset) });
+ } else {
+ data.push({ rxBytes: cursor.value.rxBytes,
+ txBytes: cursor.value.txBytes,
+ date: new Date(cursor.value.timestamp + offset) });
+ }
+ }
+ cursor.continue();
+ return;
+ }
+ this.fillResultSamples(start + offset, end + offset, data);
+ aTxn.result.data = data;
+ }.bind(this); // openCursor(range).onsuccess() callback
+ }.bind(this); // openCursor(range).onsuccess() callback
+ }.bind(this), aResultCb);
+ },
+
+ /*
+ * Fill data array (samples from database) with empty samples to match
+ * requested start / end dates.
+ */
+ fillResultSamples: function fillResultSamples(aStart, aEnd, aData) {
+ if (aData.length == 0) {
+ aData.push({ rxBytes: undefined,
+ txBytes: undefined,
+ date: new Date(aStart) });
+ }
+
+ while (aStart < aData[0].date.getTime()) {
+ aData.unshift({ rxBytes: undefined,
+ txBytes: undefined,
+ date: new Date(aData[0].date.getTime() - SAMPLE_RATE) });
+ }
+
+ while (aEnd > aData[aData.length - 1].date.getTime()) {
+ aData.push({ rxBytes: undefined,
+ txBytes: undefined,
+ date: new Date(aData[aData.length - 1].date.getTime() + SAMPLE_RATE) });
+ }
+ },
+
+ getAvailableNetworks: function getAvailableNetworks(aResultCb) {
+ this.dbNewTxn(STATS_STORE_NAME, "readonly", function(aTxn, aStore) {
+ if (!aTxn.result) {
+ aTxn.result = [];
+ }
+
+ let request = aStore.index("network").openKeyCursor(null, "nextunique");
+ request.onsuccess = function onsuccess(event) {
+ let cursor = event.target.result;
+ if (cursor) {
+ aTxn.result.push({ id: cursor.key[0],
+ type: cursor.key[1] });
+ cursor.continue();
+ return;
+ }
+ };
+ }, aResultCb);
+ },
+
+ isNetworkAvailable: function isNetworkAvailable(aNetwork, aResultCb) {
+ this.dbNewTxn(STATS_STORE_NAME, "readonly", function(aTxn, aStore) {
+ if (!aTxn.result) {
+ aTxn.result = false;
+ }
+
+ let network = [aNetwork.id, aNetwork.type];
+ let request = aStore.index("network").openKeyCursor(IDBKeyRange.only(network));
+ request.onsuccess = function onsuccess(event) {
+ if (event.target.result) {
+ aTxn.result = true;
+ }
+ };
+ }, aResultCb);
+ },
+
+ getAvailableServiceTypes: function getAvailableServiceTypes(aResultCb) {
+ this.dbNewTxn(STATS_STORE_NAME, "readonly", function(aTxn, aStore) {
+ if (!aTxn.result) {
+ aTxn.result = [];
+ }
+
+ let request = aStore.index("serviceType").openKeyCursor(null, "nextunique");
+ request.onsuccess = function onsuccess(event) {
+ let cursor = event.target.result;
+ if (cursor && cursor.key != "") {
+ aTxn.result.push({ serviceType: cursor.key });
+ cursor.continue();
+ return;
+ }
+ };
+ }, aResultCb);
+ },
+
+ get sampleRate () {
+ return SAMPLE_RATE;
+ },
+
+ get maxStorageSamples () {
+ return VALUES_MAX_LENGTH;
+ },
+
+ logAllRecords: function logAllRecords(aResultCb) {
+ this.dbNewTxn(STATS_STORE_NAME, "readonly", function(aTxn, aStore) {
+ aStore.mozGetAll().onsuccess = function onsuccess(event) {
+ aTxn.result = event.target.result;
+ };
+ }, aResultCb);
+ },
+
+ alarmToRecord: function alarmToRecord(aAlarm) {
+ let record = { networkId: aAlarm.networkId,
+ absoluteThreshold: aAlarm.absoluteThreshold,
+ relativeThreshold: aAlarm.relativeThreshold,
+ startTime: aAlarm.startTime,
+ data: aAlarm.data,
+ manifestURL: aAlarm.manifestURL,
+ pageURL: aAlarm.pageURL };
+
+ if (aAlarm.id) {
+ record.id = aAlarm.id;
+ }
+
+ return record;
+ },
+
+ recordToAlarm: function recordToalarm(aRecord) {
+ let alarm = { networkId: aRecord.networkId,
+ absoluteThreshold: aRecord.absoluteThreshold,
+ relativeThreshold: aRecord.relativeThreshold,
+ startTime: aRecord.startTime,
+ data: aRecord.data,
+ manifestURL: aRecord.manifestURL,
+ pageURL: aRecord.pageURL };
+
+ if (aRecord.id) {
+ alarm.id = aRecord.id;
+ }
+
+ return alarm;
+ },
+
+ addAlarm: function addAlarm(aAlarm, aResultCb) {
+ this.dbNewTxn(ALARMS_STORE_NAME, "readwrite", function(txn, store) {
+ if (DEBUG) {
+ debug("Going to add " + JSON.stringify(aAlarm));
+ }
+
+ let record = this.alarmToRecord(aAlarm);
+ store.put(record).onsuccess = function setResult(aEvent) {
+ txn.result = aEvent.target.result;
+ if (DEBUG) {
+ debug("Request successful. New record ID: " + txn.result);
+ }
+ };
+ }.bind(this), aResultCb);
+ },
+
+ getFirstAlarm: function getFirstAlarm(aNetworkId, aResultCb) {
+ let self = this;
+
+ this.dbNewTxn(ALARMS_STORE_NAME, "readonly", function(txn, store) {
+ if (DEBUG) {
+ debug("Get first alarm for network " + aNetworkId);
+ }
+
+ let lowerFilter = [aNetworkId, 0];
+ let upperFilter = [aNetworkId, ""];
+ let range = IDBKeyRange.bound(lowerFilter, upperFilter);
+
+ store.index("alarm").openCursor(range).onsuccess = function onsuccess(event) {
+ let cursor = event.target.result;
+ txn.result = null;
+ if (cursor) {
+ txn.result = self.recordToAlarm(cursor.value);
+ }
+ };
+ }, aResultCb);
+ },
+
+ removeAlarm: function removeAlarm(aAlarmId, aManifestURL, aResultCb) {
+ this.dbNewTxn(ALARMS_STORE_NAME, "readwrite", function(txn, store) {
+ if (DEBUG) {
+ debug("Remove alarm " + aAlarmId);
+ }
+
+ store.get(aAlarmId).onsuccess = function onsuccess(event) {
+ let record = event.target.result;
+ txn.result = false;
+ if (!record || (aManifestURL && record.manifestURL != aManifestURL)) {
+ return;
+ }
+
+ store.delete(aAlarmId);
+ txn.result = true;
+ }
+ }, aResultCb);
+ },
+
+ removeAlarms: function removeAlarms(aManifestURL, aResultCb) {
+ this.dbNewTxn(ALARMS_STORE_NAME, "readwrite", function(txn, store) {
+ if (DEBUG) {
+ debug("Remove alarms of " + aManifestURL);
+ }
+
+ store.index("manifestURL").openCursor(aManifestURL)
+ .onsuccess = function onsuccess(event) {
+ let cursor = event.target.result;
+ if (cursor) {
+ cursor.delete();
+ cursor.continue();
+ }
+ }
+ }, aResultCb);
+ },
+
+ updateAlarm: function updateAlarm(aAlarm, aResultCb) {
+ let self = this;
+ this.dbNewTxn(ALARMS_STORE_NAME, "readwrite", function(txn, store) {
+ if (DEBUG) {
+ debug("Update alarm " + aAlarm.id);
+ }
+
+ let record = self.alarmToRecord(aAlarm);
+ store.openCursor(record.id).onsuccess = function onsuccess(event) {
+ let cursor = event.target.result;
+ txn.result = false;
+ if (cursor) {
+ cursor.update(record);
+ txn.result = true;
+ }
+ }
+ }, aResultCb);
+ },
+
+ getAlarms: function getAlarms(aNetworkId, aManifestURL, aResultCb) {
+ let self = this;
+ this.dbNewTxn(ALARMS_STORE_NAME, "readonly", function(txn, store) {
+ if (DEBUG) {
+ debug("Get alarms for " + aManifestURL);
+ }
+
+ txn.result = [];
+ store.index("manifestURL").openCursor(aManifestURL)
+ .onsuccess = function onsuccess(event) {
+ let cursor = event.target.result;
+ if (!cursor) {
+ return;
+ }
+
+ if (!aNetworkId || cursor.value.networkId == aNetworkId) {
+ txn.result.push(self.recordToAlarm(cursor.value));
+ }
+
+ cursor.continue();
+ }
+ }, aResultCb);
+ },
+
+ _resetAlarms: function _resetAlarms(aNetworkId, aResultCb) {
+ this.dbNewTxn(ALARMS_STORE_NAME, "readwrite", function(txn, store) {
+ if (DEBUG) {
+ debug("Reset alarms for network " + aNetworkId);
+ }
+
+ let lowerFilter = [aNetworkId, 0];
+ let upperFilter = [aNetworkId, ""];
+ let range = IDBKeyRange.bound(lowerFilter, upperFilter);
+
+ store.index("alarm").openCursor(range).onsuccess = function onsuccess(event) {
+ let cursor = event.target.result;
+ if (cursor) {
+ if (cursor.value.startTime) {
+ cursor.value.relativeThreshold = cursor.value.threshold;
+ cursor.update(cursor.value);
+ }
+ cursor.continue();
+ return;
+ }
+ };
+ }, aResultCb);
+ }
+};