Wed, 31 Dec 2014 06:09:35 +0100
Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.
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);
1008 }
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;
1016 }
1018 if (!aNetworkId || cursor.value.networkId == aNetworkId) {
1019 txn.result.push(self.recordToAlarm(cursor.value));
1020 }
1022 cursor.continue();
1023 }
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);
1031 }
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);
1043 }
1044 cursor.continue();
1045 return;
1046 }
1047 };
1048 }, aResultCb);
1049 }
1050 };