|
1 /* This Source Code Form is subject to the terms of the Mozilla Public |
|
2 * License, v. 2.0. If a copy of the MPL was not distributed with this file, |
|
3 * You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
4 |
|
5 "use strict"; |
|
6 |
|
7 this.EXPORTED_SYMBOLS = ['NetworkStatsDB']; |
|
8 |
|
9 const DEBUG = false; |
|
10 function debug(s) { dump("-*- NetworkStatsDB: " + s + "\n"); } |
|
11 |
|
12 const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; |
|
13 |
|
14 Cu.import("resource://gre/modules/Services.jsm"); |
|
15 Cu.import("resource://gre/modules/IndexedDBHelper.jsm"); |
|
16 Cu.importGlobalProperties(["indexedDB"]); |
|
17 |
|
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"; |
|
23 |
|
24 // Constant defining the maximum values allowed per interface. If more, older |
|
25 // will be erased. |
|
26 const VALUES_MAX_LENGTH = 6 * 30; |
|
27 |
|
28 // Constant defining the rate of the samples. Daily. |
|
29 const SAMPLE_RATE = 1000 * 60 * 60 * 24; |
|
30 |
|
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 } |
|
37 |
|
38 NetworkStatsDB.prototype = { |
|
39 __proto__: IndexedDBHelper.prototype, |
|
40 |
|
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 }, |
|
50 |
|
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 */ |
|
62 |
|
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. |
|
82 |
|
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 } |
|
91 |
|
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 }); |
|
101 |
|
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 } |
|
129 |
|
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); |
|
137 |
|
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 = {}; |
|
142 |
|
143 objectStore.openCursor().onsuccess = function(event) { |
|
144 let cursor = event.target.result; |
|
145 if (!cursor){ |
|
146 return; |
|
147 } |
|
148 |
|
149 cursor.value.rxSystemBytes = cursor.value.rxTotalBytes; |
|
150 cursor.value.txSystemBytes = cursor.value.txTotalBytes; |
|
151 |
|
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 } |
|
162 |
|
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 } |
|
170 |
|
171 counters[netId].rxCounter += rxDiff; |
|
172 counters[netId].txCounter += txDiff; |
|
173 cursor.value.rxTotalBytes = counters[netId].rxCounter; |
|
174 cursor.value.txTotalBytes = counters[netId].txCounter; |
|
175 |
|
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 } |
|
182 |
|
183 cursor.update(cursor.value); |
|
184 cursor.continue(); |
|
185 }; |
|
186 |
|
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 }); |
|
191 |
|
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 }); |
|
205 |
|
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 } |
|
214 |
|
215 let newStats = cursor.value; |
|
216 newStats.serviceType = ""; |
|
217 newObjectStore.put(newStats); |
|
218 cursor.continue(); |
|
219 }; |
|
220 |
|
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); |
|
229 |
|
230 // Delete "alarm" index. |
|
231 if (alarmsStore.indexNames.contains("alarm")) { |
|
232 alarmsStore.deleteIndex("alarm"); |
|
233 } |
|
234 |
|
235 // Create new "alarm" index. |
|
236 alarmsStore.createIndex("alarm", ['networkId','relativeThreshold'], { unique: false }); |
|
237 |
|
238 // Populate new "alarm" index attributes. |
|
239 alarmsStore.openCursor().onsuccess = function(event) { |
|
240 let cursor = event.target.result; |
|
241 if (!cursor) { |
|
242 return; |
|
243 } |
|
244 |
|
245 cursor.value.relativeThreshold = cursor.value.threshold; |
|
246 cursor.value.absoluteThreshold = cursor.value.threshold; |
|
247 delete cursor.value.threshold; |
|
248 |
|
249 cursor.update(cursor.value); |
|
250 cursor.continue(); |
|
251 } |
|
252 |
|
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 } |
|
266 |
|
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); |
|
271 |
|
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 } |
|
279 |
|
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 } |
|
293 |
|
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 } |
|
301 |
|
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 }); |
|
316 |
|
317 if (DEBUG) { |
|
318 debug("Create index of 'serviceType' for version 8"); |
|
319 } |
|
320 } |
|
321 } |
|
322 }, |
|
323 |
|
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 }; |
|
335 |
|
336 return stats; |
|
337 }, |
|
338 |
|
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 }; |
|
349 |
|
350 return stats; |
|
351 }, |
|
352 |
|
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 }, |
|
360 |
|
361 saveStats: function saveStats(aStats, aResultCb) { |
|
362 let isAccumulative = aStats.isAccumulative; |
|
363 let timestamp = this.normalizeDate(aStats.date); |
|
364 |
|
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 }; |
|
376 |
|
377 stats = this.importData(stats); |
|
378 |
|
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 } |
|
384 |
|
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. |
|
390 |
|
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 } |
|
399 |
|
400 this._saveStats(aTxn, aStore, stats); |
|
401 return; |
|
402 } |
|
403 |
|
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 } |
|
410 |
|
411 // There are old samples |
|
412 if (DEBUG) { |
|
413 debug("Last value " + JSON.stringify(value)); |
|
414 } |
|
415 |
|
416 // Remove stats previous to now - VALUE_MAX_LENGTH |
|
417 this._removeOldStats(aTxn, aStore, stats.appId, stats.serviceType, |
|
418 stats.network, stats.timestamp); |
|
419 |
|
420 // Process stats before save |
|
421 this._processSamplesDiff(aTxn, aStore, cursor, stats, isAccumulative); |
|
422 }.bind(this); |
|
423 }.bind(this), aResultCb); |
|
424 }, |
|
425 |
|
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; |
|
436 |
|
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 } |
|
445 |
|
446 if (DEBUG) { |
|
447 debug("New: " + aNewSample.timestamp + " - Last: " + |
|
448 lastSample.timestamp + " - diff: " + diff); |
|
449 } |
|
450 |
|
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; |
|
467 |
|
468 aNewSample.rxTotalBytes = lastSample.rxTotalBytes + rxDiff; |
|
469 aNewSample.txTotalBytes = lastSample.txTotalBytes + txDiff; |
|
470 } else { |
|
471 rxDiff = aNewSample.rxBytes; |
|
472 txDiff = aNewSample.txBytes; |
|
473 } |
|
474 |
|
475 if (diff == 1) { |
|
476 // New element. |
|
477 |
|
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 } |
|
485 |
|
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 } |
|
496 |
|
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 }; |
|
510 |
|
511 data.push(sample); |
|
512 } |
|
513 |
|
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. |
|
523 |
|
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; |
|
532 |
|
533 if (DEBUG) { |
|
534 debug("Update: " + JSON.stringify(lastSample)); |
|
535 } |
|
536 let req = aLastSampleCursor.update(lastSample); |
|
537 } |
|
538 }, |
|
539 |
|
540 _saveStats: function _saveStats(aTxn, aStore, aNetworkStats) { |
|
541 if (DEBUG) { |
|
542 debug("_saveStats: " + JSON.stringify(aNetworkStats)); |
|
543 } |
|
544 |
|
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 }, |
|
554 |
|
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; |
|
564 |
|
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 } |
|
573 |
|
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 }, |
|
593 |
|
594 clearInterfaceStats: function clearInterfaceStats(aNetwork, aResultCb) { |
|
595 let network = [aNetwork.network.id, aNetwork.network.type]; |
|
596 let self = this; |
|
597 |
|
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 } |
|
608 |
|
609 cursor.delete(); |
|
610 cursor.continue(); |
|
611 return; |
|
612 } |
|
613 |
|
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; |
|
624 |
|
625 self._saveStats(aTxn, aStore, sample); |
|
626 } |
|
627 }; |
|
628 }, this._resetAlarms.bind(this, aNetwork.networkId, aResultCb)); |
|
629 }, |
|
630 |
|
631 clearStats: function clearStats(aNetworks, aResultCb) { |
|
632 let index = 0; |
|
633 let stats = []; |
|
634 let self = this; |
|
635 |
|
636 let callback = function(aError, aResult) { |
|
637 index++; |
|
638 |
|
639 if (!aError && index < aNetworks.length) { |
|
640 self.clearInterfaceStats(aNetworks[index], callback); |
|
641 return; |
|
642 } |
|
643 |
|
644 aResultCb(aError, aResult); |
|
645 }; |
|
646 |
|
647 if (!aNetworks[index]) { |
|
648 aResultCb(null, true); |
|
649 return; |
|
650 } |
|
651 this.clearInterfaceStats(aNetworks[index], callback); |
|
652 }, |
|
653 |
|
654 getCurrentStats: function getCurrentStats(aNetwork, aDate, aResultCb) { |
|
655 if (DEBUG) { |
|
656 debug("Get current stats for " + JSON.stringify(aNetwork) + " since " + aDate); |
|
657 } |
|
658 |
|
659 let network = [aNetwork.id, aNetwork.type]; |
|
660 if (aDate) { |
|
661 this._getCurrentStatsFromDate(network, aDate, aResultCb); |
|
662 return; |
|
663 } |
|
664 |
|
665 this._getCurrentStats(network, aResultCb); |
|
666 }, |
|
667 |
|
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"); |
|
674 |
|
675 let result = { rxBytes: 0, txBytes: 0, |
|
676 rxTotalBytes: 0, txTotalBytes: 0 }; |
|
677 |
|
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 } |
|
684 |
|
685 txn.result = result; |
|
686 }; |
|
687 }.bind(this), aResultCb); |
|
688 }, |
|
689 |
|
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()]; |
|
697 |
|
698 let range = IDBKeyRange.upperBound(upperFilter, false); |
|
699 |
|
700 let result = { rxBytes: 0, txBytes: 0, |
|
701 rxTotalBytes: 0, txTotalBytes: 0 }; |
|
702 |
|
703 request = store.openCursor(range, "prev"); |
|
704 |
|
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 } |
|
711 |
|
712 let timestamp = cursor.value.timestamp; |
|
713 let range = IDBKeyRange.lowerBound(lowerFilter, false); |
|
714 request = store.openCursor(range); |
|
715 |
|
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 } |
|
728 |
|
729 txn.result = result; |
|
730 }; |
|
731 }; |
|
732 }.bind(this), aResultCb); |
|
733 }, |
|
734 |
|
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); |
|
740 |
|
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 } |
|
748 |
|
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); |
|
754 |
|
755 let data = []; |
|
756 |
|
757 if (!aTxn.result) { |
|
758 aTxn.result = {}; |
|
759 } |
|
760 |
|
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 } |
|
770 |
|
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); |
|
774 |
|
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 }, |
|
784 |
|
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 } |
|
795 |
|
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 } |
|
801 |
|
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 }, |
|
808 |
|
809 getAvailableNetworks: function getAvailableNetworks(aResultCb) { |
|
810 this.dbNewTxn(STATS_STORE_NAME, "readonly", function(aTxn, aStore) { |
|
811 if (!aTxn.result) { |
|
812 aTxn.result = []; |
|
813 } |
|
814 |
|
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 }, |
|
827 |
|
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 } |
|
833 |
|
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 }, |
|
843 |
|
844 getAvailableServiceTypes: function getAvailableServiceTypes(aResultCb) { |
|
845 this.dbNewTxn(STATS_STORE_NAME, "readonly", function(aTxn, aStore) { |
|
846 if (!aTxn.result) { |
|
847 aTxn.result = []; |
|
848 } |
|
849 |
|
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 }, |
|
861 |
|
862 get sampleRate () { |
|
863 return SAMPLE_RATE; |
|
864 }, |
|
865 |
|
866 get maxStorageSamples () { |
|
867 return VALUES_MAX_LENGTH; |
|
868 }, |
|
869 |
|
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 }, |
|
877 |
|
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 }; |
|
886 |
|
887 if (aAlarm.id) { |
|
888 record.id = aAlarm.id; |
|
889 } |
|
890 |
|
891 return record; |
|
892 }, |
|
893 |
|
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 }; |
|
902 |
|
903 if (aRecord.id) { |
|
904 alarm.id = aRecord.id; |
|
905 } |
|
906 |
|
907 return alarm; |
|
908 }, |
|
909 |
|
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 } |
|
915 |
|
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 }, |
|
925 |
|
926 getFirstAlarm: function getFirstAlarm(aNetworkId, aResultCb) { |
|
927 let self = this; |
|
928 |
|
929 this.dbNewTxn(ALARMS_STORE_NAME, "readonly", function(txn, store) { |
|
930 if (DEBUG) { |
|
931 debug("Get first alarm for network " + aNetworkId); |
|
932 } |
|
933 |
|
934 let lowerFilter = [aNetworkId, 0]; |
|
935 let upperFilter = [aNetworkId, ""]; |
|
936 let range = IDBKeyRange.bound(lowerFilter, upperFilter); |
|
937 |
|
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 }, |
|
947 |
|
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 } |
|
953 |
|
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 } |
|
960 |
|
961 store.delete(aAlarmId); |
|
962 txn.result = true; |
|
963 } |
|
964 }, aResultCb); |
|
965 }, |
|
966 |
|
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 } |
|
972 |
|
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 }, |
|
983 |
|
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 } |
|
990 |
|
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 }, |
|
1002 |
|
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 } |
|
1009 |
|
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 } |
|
1017 |
|
1018 if (!aNetworkId || cursor.value.networkId == aNetworkId) { |
|
1019 txn.result.push(self.recordToAlarm(cursor.value)); |
|
1020 } |
|
1021 |
|
1022 cursor.continue(); |
|
1023 } |
|
1024 }, aResultCb); |
|
1025 }, |
|
1026 |
|
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 } |
|
1032 |
|
1033 let lowerFilter = [aNetworkId, 0]; |
|
1034 let upperFilter = [aNetworkId, ""]; |
|
1035 let range = IDBKeyRange.bound(lowerFilter, upperFilter); |
|
1036 |
|
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 }; |