Bug 1070944 - Part 3: Implementation of NetworkStatsDB and querying statistics. r=albert

This commit is contained in:
Ethan Tseng 2015-02-09 10:17:03 +08:00
parent c940ba7846
commit 3f1a20c572
3 changed files with 177 additions and 58 deletions

View File

@ -11,14 +11,24 @@ function debug(s) { dump("-*- NetworkStatsDB: " + s + "\n"); }
const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/IndexedDBHelper.jsm");
Cu.importGlobalProperties(["indexedDB"]);
XPCOMUtils.defineLazyServiceGetter(this, "appsService",
"@mozilla.org/AppsService;1",
"nsIAppsService");
const DB_NAME = "net_stats";
const DB_VERSION = 8;
const DEPRECATED_STORE_NAME = "net_stats";
const STATS_STORE_NAME = "net_stats_store";
const DB_VERSION = 9;
const DEPRECATED_STATS_STORE_NAME =
[
"net_stats_v2", // existed only in DB version 2
"net_stats", // existed in DB version 1 and 3 to 5
"net_stats_store", // existed in DB version 6 to 8
];
const STATS_STORE_NAME = "net_stats_store_v3"; // since DB version 9
const ALARMS_STORE_NAME = "net_alarm";
// Constant defining the maximum values allowed per interface. If more, older
@ -75,7 +85,8 @@ NetworkStatsDB.prototype = {
if (DEBUG) debug("Upgrade 0 to 1: Create object stores and indexes.");
// Create the initial database schema.
objectStore = db.createObjectStore(DEPRECATED_STORE_NAME, { keyPath: ["connectionType", "timestamp"] });
objectStore = db.createObjectStore(DEPRECATED_STATS_STORE_NAME[1],
{ keyPath: ["connectionType", "timestamp"] });
objectStore.createIndex("connectionType", "connectionType", { unique: false });
objectStore.createIndex("timestamp", "timestamp", { unique: false });
objectStore.createIndex("rxBytes", "rxBytes", { unique: false });
@ -106,13 +117,17 @@ NetworkStatsDB.prototype = {
// In version 1.2 objectStore name was 'net_stats_v2', to avoid errors when
// upgrading from 1.2 to 1.3 objectStore name should be checked.
let stores = db.objectStoreNames;
if(stores.contains("net_stats_v2")) {
db.deleteObjectStore("net_stats_v2");
let deprecatedName = DEPRECATED_STATS_STORE_NAME[0];
let storeName = DEPRECATED_STATS_STORE_NAME[1];
if(stores.contains(deprecatedName)) {
// Delete the obsolete stats store.
db.deleteObjectStore(deprecatedName);
} else {
db.deleteObjectStore(DEPRECATED_STORE_NAME);
// Re-create stats object store without copying records.
db.deleteObjectStore(storeName);
}
objectStore = db.createObjectStore(DEPRECATED_STORE_NAME, { keyPath: ["appId", "network", "timestamp"] });
objectStore = db.createObjectStore(storeName, { keyPath: ["appId", "network", "timestamp"] });
objectStore.createIndex("appId", "appId", { unique: false });
objectStore.createIndex("network", "network", { unique: false });
objectStore.createIndex("networkType", "networkType", { unique: false });
@ -129,7 +144,7 @@ NetworkStatsDB.prototype = {
if (DEBUG) debug("Upgrade 3 to 4: Delete redundant indexes.");
// Delete redundant indexes (leave "network" only).
objectStore = aTransaction.objectStore(DEPRECATED_STORE_NAME);
objectStore = aTransaction.objectStore(DEPRECATED_STATS_STORE_NAME[1]);
if (objectStore.indexNames.contains("appId")) {
objectStore.deleteIndex("appId");
}
@ -160,7 +175,7 @@ NetworkStatsDB.prototype = {
// In order to manage alarms, it is necessary to use a global counter
// (totalBytes) that will increase regardless of the system reboot.
objectStore = aTransaction.objectStore(DEPRECATED_STORE_NAME);
objectStore = aTransaction.objectStore(DEPRECATED_STATS_STORE_NAME[1]);
// Now, systemBytes will hold the old totalBytes and totalBytes will
// keep the increasing counter. |counters| will keep the track of
@ -228,16 +243,17 @@ NetworkStatsDB.prototype = {
// saved by service type (e.g., Tethering, OTA). Thus it's needed to
// have a new key ("serviceType") for the ojectStore.
let newObjectStore;
newObjectStore = db.createObjectStore(STATS_STORE_NAME,
let deprecatedName = DEPRECATED_STATS_STORE_NAME[1];
newObjectStore = db.createObjectStore(DEPRECATED_STATS_STORE_NAME[2],
{ keyPath: ["appId", "serviceType", "network", "timestamp"] });
newObjectStore.createIndex("network", "network", { unique: false });
// Copy the data from the original objectStore to the new objectStore.
objectStore = aTransaction.objectStore(DEPRECATED_STORE_NAME);
objectStore = aTransaction.objectStore(deprecatedName);
objectStore.openCursor().onsuccess = function(event) {
let cursor = event.target.result;
if (!cursor) {
db.deleteObjectStore(DEPRECATED_STORE_NAME);
db.deleteObjectStore(deprecatedName);
// upgrade5to6 completed now.
upgradeNextVersion();
return;
@ -287,7 +303,7 @@ NetworkStatsDB.prototype = {
// Previous versions save accumulative totalBytes, increasing although the system
// reboots or resets stats. But is necessary to reset the total counters when reset
// through 'clearInterfaceStats'.
let statsStore = aTransaction.objectStore(STATS_STORE_NAME);
let statsStore = aTransaction.objectStore(DEPRECATED_STATS_STORE_NAME[2]);
let networks = [];
// Find networks stored in the database.
@ -376,9 +392,54 @@ NetworkStatsDB.prototype = {
if (DEBUG) debug("Upgrade 7 to 8: Create index serviceType.");
// Create index for 'ServiceType' in order to make it retrievable.
let statsStore = aTransaction.objectStore(STATS_STORE_NAME);
let statsStore = aTransaction.objectStore(DEPRECATED_STATS_STORE_NAME[2]);
statsStore.createIndex("serviceType", "serviceType", { unique: false });
upgradeNextVersion();
},
function upgrade8to9() {
if (DEBUG) debug("Upgrade 8 to 9: Add keyPath isInBrowser to " +
"network stats object store");
// Since B2G v2.0, there is no stand-alone browser app anymore.
// The browser app is a mozbrowser iframe element owned by system app.
// In order to separate traffic generated from system and browser, we
// have to add a new attribute |isInBrowser| as keyPath.
// Refer to bug 1070944 for more detail.
let newObjectStore;
let deprecatedName = DEPRECATED_STATS_STORE_NAME[2];
newObjectStore = db.createObjectStore(STATS_STORE_NAME,
{ keyPath: ["appId", "isInBrowser", "serviceType",
"network", "timestamp"] });
newObjectStore.createIndex("network", "network", { unique: false });
newObjectStore.createIndex("serviceType", "serviceType", { unique: false });
// Copy records from the current object store to the new one.
objectStore = aTransaction.objectStore(deprecatedName);
objectStore.openCursor().onsuccess = function (event) {
let cursor = event.target.result;
if (!cursor) {
db.deleteObjectStore(deprecatedName);
// upgrade8to9 completed now.
return;
}
let newStats = cursor.value;
// Augment records by adding the new isInBrowser attribute.
// Notes:
// 1. Key value cannot be boolean type. Use 1/0 instead of true/false.
// 2. Most traffic of system app should come from its browser iframe,
// thus assign isInBrowser as 1 for system app.
let manifestURL = appsService.getManifestURLByLocalId(newStats.appId);
if (manifestURL && manifestURL.search(/app:\/\/system\./) === 0) {
newStats.isInBrowser = 1;
} else {
newStats.isInBrowser = 0;
}
newObjectStore.put(newStats);
cursor.continue();
};
}
];
let index = aOldVersion;
@ -412,6 +473,7 @@ NetworkStatsDB.prototype = {
importData: function importData(aStats) {
let stats = { appId: aStats.appId,
isInBrowser: aStats.isInBrowser ? 1 : 0,
serviceType: aStats.serviceType,
network: [aStats.networkId, aStats.networkType],
timestamp: aStats.timestamp,
@ -427,6 +489,7 @@ NetworkStatsDB.prototype = {
exportData: function exportData(aStats) {
let stats = { appId: aStats.appId,
isInBrowser: aStats.isInBrowser ? true : false,
serviceType: aStats.serviceType,
networkId: aStats.network[0],
networkType: aStats.network[1],
@ -452,16 +515,17 @@ NetworkStatsDB.prototype = {
let timestamp = this.normalizeDate(aStats.date);
let stats = { appId: aStats.appId,
isInBrowser: aStats.isInBrowser,
serviceType: aStats.serviceType,
networkId: aStats.networkId,
networkType: aStats.networkType,
timestamp: timestamp,
rxBytes: (isAccumulative) ? 0 : aStats.rxBytes,
txBytes: (isAccumulative) ? 0 : aStats.txBytes,
rxSystemBytes: (isAccumulative) ? aStats.rxBytes : 0,
txSystemBytes: (isAccumulative) ? aStats.txBytes : 0,
rxTotalBytes: (isAccumulative) ? aStats.rxBytes : 0,
txTotalBytes: (isAccumulative) ? aStats.txBytes : 0 };
rxBytes: isAccumulative ? 0 : aStats.rxBytes,
txBytes: isAccumulative ? 0 : aStats.txBytes,
rxSystemBytes: isAccumulative ? aStats.rxBytes : 0,
txSystemBytes: isAccumulative ? aStats.txBytes : 0,
rxTotalBytes: isAccumulative ? aStats.rxBytes : 0,
txTotalBytes: isAccumulative ? aStats.txBytes : 0 };
stats = this.importData(stats);
@ -471,8 +535,10 @@ NetworkStatsDB.prototype = {
debug("New stats: " + JSON.stringify(stats));
}
let lowerFilter = [stats.appId, stats.serviceType, stats.network, 0];
let upperFilter = [stats.appId, stats.serviceType, stats.network, ""];
let lowerFilter = [stats.appId, stats.isInBrowser, stats.serviceType,
stats.network, 0];
let upperFilter = [stats.appId, stats.isInBrowser, stats.serviceType,
stats.network, ""];
let range = IDBKeyRange.bound(lowerFilter, upperFilter, false, false);
let request = aStore.openCursor(range, 'prev');
@ -500,8 +566,8 @@ NetworkStatsDB.prototype = {
}
// Remove stats previous to now - VALUE_MAX_LENGTH
this._removeOldStats(aTxn, aStore, stats.appId, stats.serviceType,
stats.network, stats.timestamp);
this._removeOldStats(aTxn, aStore, stats.appId, stats.isInBrowser,
stats.serviceType, stats.network, stats.timestamp);
// Process stats before save
this._processSamplesDiff(aTxn, aStore, cursor, stats, isAccumulative);
@ -584,6 +650,7 @@ NetworkStatsDB.prototype = {
for (let i = diff - 2; i >= 0; i--) {
let time = aNewSample.timestamp - SAMPLE_RATE * (i + 1);
let sample = { appId: aNewSample.appId,
isInBrowser: aNewSample.isInBrowser,
serviceType: aNewSample.serviceType,
network: aNewSample.network,
timestamp: time,
@ -638,12 +705,12 @@ NetworkStatsDB.prototype = {
}
},
_removeOldStats: function _removeOldStats(aTxn, aStore, aAppId, aServiceType,
aNetwork, aDate) {
_removeOldStats: function _removeOldStats(aTxn, aStore, aAppId, aIsInBrowser,
aServiceType, aNetwork, aDate) {
// Callback function to remove old items when new ones are added.
let filterDate = aDate - (SAMPLE_RATE * VALUES_MAX_LENGTH - 1);
let lowerFilter = [aAppId, aServiceType, aNetwork, 0];
let upperFilter = [aAppId, aServiceType, aNetwork, filterDate];
let lowerFilter = [aAppId, aIsInBrowser, aServiceType, aNetwork, 0];
let upperFilter = [aAppId, aIsInBrowser, aServiceType, aNetwork, filterDate];
let range = IDBKeyRange.bound(lowerFilter, upperFilter, false, false);
let lastSample = null;
let self = this;
@ -702,6 +769,7 @@ NetworkStatsDB.prototype = {
timestamp = self.normalizeDate(timestamp);
sample.timestamp = timestamp;
sample.appId = 0;
sample.isInBrowser = 0;
sample.serviceType = "";
sample.rxBytes = 0;
sample.txBytes = 0;
@ -754,13 +822,13 @@ NetworkStatsDB.prototype = {
_getCurrentStats: function _getCurrentStats(aNetwork, aResultCb) {
this.dbNewTxn(STATS_STORE_NAME, "readonly", function(txn, store) {
let request = null;
let upperFilter = [0, "", aNetwork, Date.now()];
let upperFilter = [0, 1, "", aNetwork, Date.now()];
let range = IDBKeyRange.upperBound(upperFilter, false);
request = store.openCursor(range, "prev");
let result = { rxBytes: 0, txBytes: 0,
rxTotalBytes: 0, txTotalBytes: 0 };
request = store.openCursor(range, "prev");
request.onsuccess = function onsuccess(event) {
let cursor = event.target.result;
if (cursor) {
@ -778,11 +846,8 @@ NetworkStatsDB.prototype = {
this.dbNewTxn(STATS_STORE_NAME, "readonly", function(txn, store) {
let request = null;
let start = this.normalizeDate(aDate);
let lowerFilter = [0, "", aNetwork, start];
let upperFilter = [0, "", aNetwork, Date.now()];
let upperFilter = [0, 1, "", aNetwork, Date.now()];
let range = IDBKeyRange.upperBound(upperFilter, false);
let result = { rxBytes: 0, txBytes: 0,
rxTotalBytes: 0, txTotalBytes: 0 };
@ -818,24 +883,32 @@ NetworkStatsDB.prototype = {
}.bind(this), aResultCb);
},
find: function find(aResultCb, aAppId, aServiceType, aNetwork,
aStart, aEnd, aAppManifestURL) {
find: function find(aResultCb, aAppId, aBrowsingTrafficOnly, aServiceType,
aNetwork, aStart, aEnd, aAppManifestURL) {
let offset = (new Date()).getTimezoneOffset() * 60 * 1000;
let start = this.normalizeDate(aStart);
let end = this.normalizeDate(aEnd);
if (DEBUG) {
debug("Find samples for appId: " + aAppId + " serviceType: " +
aServiceType + " network: " + JSON.stringify(aNetwork) + " from " +
start + " until " + end);
debug("Find samples for appId: " + aAppId +
" browsingTrafficOnly: " + aBrowsingTrafficOnly +
" serviceType: " + aServiceType +
" network: " + JSON.stringify(aNetwork) + " from " + start +
" until " + end);
debug("Start time: " + new Date(start));
debug("End time: " + new Date(end));
}
// Find samples of browsing traffic (isInBrowser = 1) first since they are
// needed no matter browsingTrafficOnly is true or false.
// We have to make two queries to database because we cannot filter correct
// records by a single query that sets ranges for two keys (isInBrowser and
// timestamp). We think it is because the keyPath contains an array
// (network) so such query does not work.
this.dbNewTxn(STATS_STORE_NAME, "readonly", function(aTxn, aStore) {
let network = [aNetwork.id, aNetwork.type];
let lowerFilter = [aAppId, aServiceType, network, start];
let upperFilter = [aAppId, aServiceType, network, end];
let lowerFilter = [aAppId, 1, aServiceType, network, start];
let upperFilter = [aAppId, 1, aServiceType, network, end];
let range = IDBKeyRange.bound(lowerFilter, upperFilter, false, false);
let data = [];
@ -843,6 +916,12 @@ NetworkStatsDB.prototype = {
if (!aTxn.result) {
aTxn.result = {};
}
aTxn.result.appManifestURL = aAppManifestURL;
aTxn.result.browsingTrafficOnly = aBrowsingTrafficOnly;
aTxn.result.serviceType = aServiceType;
aTxn.result.network = aNetwork;
aTxn.result.start = aStart;
aTxn.result.end = aEnd;
let request = aStore.openCursor(range).onsuccess = function(event) {
var cursor = event.target.result;
@ -854,17 +933,43 @@ NetworkStatsDB.prototype = {
return;
}
// When requested samples (start / end) are not in the range of now and
// now - VALUES_MAX_LENGTH, fill with empty samples.
this.fillResultSamples(start + offset, end + offset, data);
if (aBrowsingTrafficOnly) {
this.fillResultSamples(start + offset, end + offset, data);
aTxn.result.data = data;
return;
}
aTxn.result.appManifestURL = aAppManifestURL;
aTxn.result.serviceType = aServiceType;
aTxn.result.network = aNetwork;
aTxn.result.start = aStart;
aTxn.result.end = aEnd;
aTxn.result.data = data;
}.bind(this);
// Find samples of app traffic (isInBrowser = 0) as well if
// browsingTrafficOnly is false.
lowerFilter = [aAppId, 0, aServiceType, network, start];
upperFilter = [aAppId, 0, aServiceType, network, end];
range = IDBKeyRange.bound(lowerFilter, upperFilter, false, false);
request = aStore.openCursor(range).onsuccess = function(event) {
cursor = event.target.result;
if (cursor) {
var date = new Date(cursor.value.timestamp + offset);
var foundData = data.find(function (element, index, array) {
if (element.date.getTime() !== date.getTime()) {
return false;
}
return element;
}, date);
if (foundData) {
foundData.rxBytes += cursor.value.rxBytes;
foundData.txBytes += cursor.value.txBytes;
} else {
data.push({ rxBytes: cursor.value.rxBytes,
txBytes: cursor.value.txBytes,
date: new Date(cursor.value.timestamp + offset) });
}
cursor.continue();
return;
}
this.fillResultSamples(start + offset, end + offset, data);
aTxn.result.data = data;
}.bind(this); // openCursor(range).onsuccess() callback
}.bind(this); // openCursor(range).onsuccess() callback
}.bind(this), aResultCb);
},

View File

@ -72,6 +72,7 @@ function NetworkStats(aWindow, aStats) {
debug("NetworkStats Constructor");
}
this.appManifestURL = aStats.appManifestURL || null;
this.browsingTrafficOnly = aStats.browsingTrafficOnly || false;
this.serviceType = aStats.serviceType || null;
this.network = new aWindow.MozNetworkStatsInterface(aStats.network);
this.start = aStats.start ? new aWindow.Date(aStats.start.getTime()) : null;
@ -138,13 +139,22 @@ NetworkStatsManager.prototype = {
}
let appManifestURL = null;
let browsingTrafficOnly = false;
let serviceType = null;
if (aOptions) {
// appManifestURL is used to query network statistics by app;
// serviceType is used to query network statistics by system service.
// It is illegal to specify both of them at the same time.
if (aOptions.appManifestURL && aOptions.serviceType) {
throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
}
// browsingTrafficOnly is meaningful only when querying by app.
if (!aOptions.appManifestURL && aOptions.browsingTrafficOnly) {
throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
}
appManifestURL = aOptions.appManifestURL;
serviceType = aOptions.serviceType;
browsingTrafficOnly = aOptions.browsingTrafficOnly || false;
}
// TODO Bug 929410 Date object cannot correctly pass through cpmm/ppmm IPC
@ -158,6 +168,7 @@ NetworkStatsManager.prototype = {
start: aStart,
end: aEnd,
appManifestURL: appManifestURL,
browsingTrafficOnly: browsingTrafficOnly,
serviceType: serviceType,
id: this.getRequestId(request) });
return request;

View File

@ -415,6 +415,7 @@ this.NetworkStatsService = {
}
}
let browsingTrafficOnly = msg.browsingTrafficOnly || false;
let serviceType = msg.serviceType || "";
let start = new Date(msg.start);
@ -424,7 +425,7 @@ this.NetworkStatsService = {
this._db.find(function onStatsFound(aError, aResult) {
mm.sendAsyncMessage("NetworkStats:Get:Return",
{ id: msg.id, error: aError, result: aResult });
}, appId, serviceType, network, start, end, appManifestURL);
}, appId, browsingTrafficOnly, serviceType, network, start, end, appManifestURL);
}).bind(this);
this.validateNetwork(network, function onValidateNetwork(aNetId) {
@ -439,6 +440,7 @@ this.NetworkStatsService = {
if (this._networks[aNetId].status == NETWORK_STATUS_READY) {
debug("getstats for network " + network.id + " of type " + network.type);
debug("appId: " + appId + " from appManifestURL: " + appManifestURL);
debug("browsingTrafficOnly: " + browsingTrafficOnly);
debug("serviceType: " + serviceType);
if (appId || serviceType) {
@ -456,7 +458,7 @@ this.NetworkStatsService = {
this._db.find(function onStatsFound(aError, aResult) {
mm.sendAsyncMessage("NetworkStats:Get:Return",
{ id: msg.id, error: aError, result: aResult });
}, appId, serviceType, network, start, end, appManifestURL);
}, appId, browsingTrafficOnly, serviceType, network, start, end, appManifestURL);
}.bind(this));
},
@ -779,10 +781,11 @@ this.NetworkStatsService = {
aStats.networkType + " " + aStats.date + " " +
aStats.rxBytes + " " + aStats.txBytes);
// Generate an unique key from |appId|, |serviceType| and |netId|,
// which is used to retrieve data in |cachedStats|.
// Generate an unique key from |appId|, |isInBrowser|, |serviceType| and
// |netId|, which is used to retrieve data in |cachedStats|.
let netId = this.getNetworkId(aStats.networkId, aStats.networkType);
let key = aStats.appId + "" + aStats.serviceType + "" + netId;
let key = aStats.appId + "" + aStats.isInBrowser + "" +
aStats.serviceType + "" + netId;
// |cachedStats| only keeps the data with the same date.
// If the incoming date is different from |cachedStatsDate|,