dom/network/src/NetworkStatsDB.jsm

Thu, 22 Jan 2015 13:21:57 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Thu, 22 Jan 2015 13:21:57 +0100
branch
TOR_BUG_9701
changeset 15
b8a032363ba2
permissions
-rw-r--r--

Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6

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

mercurial