Bug 1070944 - Part 3: Implementation of NetworkStatsDB and querying statistics. r=albert
authorEthan Tseng <ettseng@mozilla.com>
Mon, 09 Feb 2015 10:17:03 +0800
changeset 228244 a67a9f37cb211c69426032b3032918943bc50836
parent 228243 07f55ff86e156526fff1910acd4326d3046c169a
child 228245 2ca4450d6a33c7247802f43a34432911b5169a2a
push id13814
push usercbook@mozilla.com
push dateTue, 10 Feb 2015 11:49:28 +0000
treeherderb2g-inbound@2ca4450d6a33 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersalbert
bugs1070944
milestone38.0a1
Bug 1070944 - Part 3: Implementation of NetworkStatsDB and querying statistics. r=albert
dom/network/NetworkStatsDB.jsm
dom/network/NetworkStatsManager.js
dom/network/NetworkStatsService.jsm
--- a/dom/network/NetworkStatsDB.jsm
+++ b/dom/network/NetworkStatsDB.jsm
@@ -6,24 +6,34 @@
 
 this.EXPORTED_SYMBOLS = ['NetworkStatsDB'];
 
 const DEBUG = false;
 function debug(s) { dump("-*- NetworkStatsDB: " + s + "\n"); }
 
 const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
 
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/IndexedDBHelper.jsm");
 Cu.importGlobalProperties(["indexedDB"]);
 
+XPCOMUtils.defineLazyServiceGetter(this, "appsService",
+                                   "@mozilla.org/AppsService;1",
+                                   "nsIAppsService");
+
 const DB_NAME = "net_stats";
-const DB_VERSION = 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
 // will be erased.
 const VALUES_MAX_LENGTH = 6 * 30;
 
 // Constant defining the rate of the samples. Daily.
 const SAMPLE_RATE = 1000 * 60 * 60 * 24;
@@ -70,17 +80,18 @@ NetworkStatsDB.prototype = {
     let objectStore;
 
     // An array of upgrade functions for each version.
     let upgradeSteps = [
       function upgrade0to1() {
         if (DEBUG) debug("Upgrade 0 to 1: Create object stores and indexes.");
 
         // Create the initial database schema.
-        objectStore = db.createObjectStore(DEPRECATED_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 });
         objectStore.createIndex("txBytes", "txBytes", { unique: false });
         objectStore.createIndex("rxTotalBytes", "rxTotalBytes", { unique: false });
         objectStore.createIndex("txTotalBytes", "txTotalBytes", { unique: false });
 
         upgradeNextVersion();
@@ -101,40 +112,44 @@ NetworkStatsDB.prototype = {
         // [networkId, networkType] not just by their connectionType,
         // to modify the keyPath is mandatory to delete the object store
         // and create it again. Old data is going to be deleted because the
         // networkId for each sample can not be set.
 
         // In version 1.2 objectStore name was 'net_stats_v2', to avoid errors when
         // upgrading from 1.2 to 1.3 objectStore name should be checked.
         let stores = db.objectStoreNames;
-        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 });
         objectStore.createIndex("timestamp", "timestamp", { unique: false });
         objectStore.createIndex("rxBytes", "rxBytes", { unique: false });
         objectStore.createIndex("txBytes", "txBytes", { unique: false });
         objectStore.createIndex("rxTotalBytes", "rxTotalBytes", { unique: false });
         objectStore.createIndex("txTotalBytes", "txTotalBytes", { unique: false });
 
         upgradeNextVersion();
       },
 
       function upgrade3to4() {
         if (DEBUG) debug("Upgrade 3 to 4: Delete redundant indexes.");
 
         // Delete redundant indexes (leave "network" only).
-        objectStore = aTransaction.objectStore(DEPRECATED_STORE_NAME);
+        objectStore = aTransaction.objectStore(DEPRECATED_STATS_STORE_NAME[1]);
         if (objectStore.indexNames.contains("appId")) {
           objectStore.deleteIndex("appId");
         }
         if (objectStore.indexNames.contains("networkType")) {
           objectStore.deleteIndex("networkType");
         }
         if (objectStore.indexNames.contains("timestamp")) {
           objectStore.deleteIndex("timestamp");
@@ -155,17 +170,17 @@ NetworkStatsDB.prototype = {
         upgradeNextVersion();
       },
 
       function upgrade4to5() {
         if (DEBUG) debug("Upgrade 4 to 5: Create object store for alarms.");
 
         // In order to manage alarms, it is necessary to use a global counter
         // (totalBytes) that will increase regardless of the system reboot.
-        objectStore = aTransaction.objectStore(DEPRECATED_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
         // accumulated values.
         let counters = {};
 
         objectStore.openCursor().onsuccess = function(event) {
           let cursor = event.target.result;
@@ -223,26 +238,27 @@ NetworkStatsDB.prototype = {
         if (DEBUG) debug("Upgrade 5 to 6: Add keyPath serviceType to object store.");
 
         // In contrast to "per-app" traffic data, "system-only" traffic data
         // refers to data which can not be identified by any applications.
         // To further support "system-only" data storage, the data can be
         // saved by service type (e.g., Tethering, OTA). Thus it's needed to
         // have a new key ("serviceType") for the ojectStore.
         let newObjectStore;
-        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;
           }
 
           let newStats = cursor.value;
           newStats.serviceType = "";
           newObjectStore.put(newStats);
@@ -282,17 +298,17 @@ NetworkStatsDB.prototype = {
           cursor.continue();
         }
 
         function upgrade6to7_updateTotalBytes() {
           if (DEBUG) debug("Upgrade 6 to 7: Update TotalBytes.");
           // Previous versions save accumulative totalBytes, increasing although the system
           // reboots or resets stats. But is necessary to reset the total counters when reset
           // through 'clearInterfaceStats'.
-          let statsStore = aTransaction.objectStore(STATS_STORE_NAME);
+          let statsStore = aTransaction.objectStore(DEPRECATED_STATS_STORE_NAME[2]);
           let networks = [];
 
           // Find networks stored in the database.
           statsStore.index("network").openKeyCursor(null, "nextunique").onsuccess = function(event) {
             let cursor = event.target.result;
 
             // Store each network into an array.
             if (cursor) {
@@ -371,19 +387,64 @@ NetworkStatsDB.prototype = {
           }; // end of statsStore.index("network").openKeyCursor().onsuccess callback
         } // end of function upgrade6to7_updateTotalBytes
       },
 
       function upgrade7to8() {
         if (DEBUG) debug("Upgrade 7 to 8: Create index serviceType.");
 
         // Create index for 'ServiceType' in order to make it retrievable.
-        let statsStore = aTransaction.objectStore(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;
     let outer = this;
 
     function upgradeNextVersion() {
       if (index == aNewVersion) {
         debug("Upgrade finished.");
@@ -407,31 +468,33 @@ NetworkStatsDB.prototype = {
       return;
     }
 
     upgradeNextVersion();
   },
 
   importData: function importData(aStats) {
     let stats = { appId:         aStats.appId,
+                  isInBrowser:   aStats.isInBrowser ? 1 : 0,
                   serviceType:   aStats.serviceType,
                   network:       [aStats.networkId, aStats.networkType],
                   timestamp:     aStats.timestamp,
                   rxBytes:       aStats.rxBytes,
                   txBytes:       aStats.txBytes,
                   rxSystemBytes: aStats.rxSystemBytes,
                   txSystemBytes: aStats.txSystemBytes,
                   rxTotalBytes:  aStats.rxTotalBytes,
                   txTotalBytes:  aStats.txTotalBytes };
 
     return stats;
   },
 
   exportData: function exportData(aStats) {
     let stats = { appId:        aStats.appId,
+                  isInBrowser:  aStats.isInBrowser ? true : false,
                   serviceType:  aStats.serviceType,
                   networkId:    aStats.network[0],
                   networkType:  aStats.network[1],
                   timestamp:    aStats.timestamp,
                   rxBytes:      aStats.rxBytes,
                   txBytes:      aStats.txBytes,
                   rxTotalBytes: aStats.rxTotalBytes,
                   txTotalBytes: aStats.txTotalBytes };
@@ -447,37 +510,40 @@ NetworkStatsDB.prototype = {
     return timestamp;
   },
 
   saveStats: function saveStats(aStats, aResultCb) {
     let isAccumulative = aStats.isAccumulative;
     let timestamp = this.normalizeDate(aStats.date);
 
     let stats = { appId:         aStats.appId,
+                  isInBrowser:   aStats.isInBrowser,
                   serviceType:   aStats.serviceType,
                   networkId:     aStats.networkId,
                   networkType:   aStats.networkType,
                   timestamp:     timestamp,
-                  rxBytes:       (isAccumulative) ? 0 : aStats.rxBytes,
-                  txBytes:       (isAccumulative) ? 0 : aStats.txBytes,
-                  rxSystemBytes: (isAccumulative) ? aStats.rxBytes : 0,
-                  txSystemBytes: (isAccumulative) ? aStats.txBytes : 0,
-                  rxTotalBytes:  (isAccumulative) ? aStats.rxBytes : 0,
-                  txTotalBytes:  (isAccumulative) ? aStats.txBytes : 0 };
+                  rxBytes:       isAccumulative ? 0 : aStats.rxBytes,
+                  txBytes:       isAccumulative ? 0 : aStats.txBytes,
+                  rxSystemBytes: isAccumulative ? aStats.rxBytes : 0,
+                  txSystemBytes: isAccumulative ? aStats.txBytes : 0,
+                  rxTotalBytes:  isAccumulative ? aStats.rxBytes : 0,
+                  txTotalBytes:  isAccumulative ? aStats.txBytes : 0 };
 
     stats = this.importData(stats);
 
     this.dbNewTxn(STATS_STORE_NAME, "readwrite", function(aTxn, aStore) {
       if (DEBUG) {
         debug("Filtered time: " + new Date(timestamp));
         debug("New stats: " + JSON.stringify(stats));
       }
 
-      let lowerFilter = [stats.appId, stats.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');
       request.onsuccess = function onsuccess(event) {
         let cursor = event.target.result;
         if (!cursor) {
           // Empty, so save first element.
 
@@ -495,18 +561,18 @@ NetworkStatsDB.prototype = {
         }
 
         // There are old samples
         if (DEBUG) {
           debug("Last value " + JSON.stringify(cursor.value));
         }
 
         // Remove stats previous to now - VALUE_MAX_LENGTH
-        this._removeOldStats(aTxn, aStore, stats.appId, stats.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);
       }.bind(this);
     }.bind(this), aResultCb);
   },
 
   /*
@@ -579,16 +645,17 @@ NetworkStatsDB.prototype = {
       if (diff > VALUES_MAX_LENGTH) {
         diff = VALUES_MAX_LENGTH;
       }
 
       let data = [];
       for (let i = diff - 2; i >= 0; i--) {
         let time = aNewSample.timestamp - SAMPLE_RATE * (i + 1);
         let sample = { appId:         aNewSample.appId,
+                       isInBrowser:   aNewSample.isInBrowser,
                        serviceType:   aNewSample.serviceType,
                        network:       aNewSample.network,
                        timestamp:     time,
                        rxBytes:       0,
                        txBytes:       0,
                        rxSystemBytes: lastSample.rxSystemBytes,
                        txSystemBytes: lastSample.txSystemBytes,
                        rxTotalBytes:  lastSample.rxTotalBytes,
@@ -633,22 +700,22 @@ NetworkStatsDB.prototype = {
       for (let i = 0; i <= len; i++) {
         aStore.put(aNetworkStats[i]);
       }
     } else {
       aStore.put(aNetworkStats);
     }
   },
 
-  _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;
 
     aStore.openCursor(range).onsuccess = function(event) {
       var cursor = event.target.result;
       if (cursor) {
         lastSample = cursor.value;
@@ -697,16 +764,17 @@ NetworkStatsDB.prototype = {
           return;
         }
 
         if (sample) {
           let timestamp = new Date();
           timestamp = self.normalizeDate(timestamp);
           sample.timestamp = timestamp;
           sample.appId = 0;
+          sample.isInBrowser = 0;
           sample.serviceType = "";
           sample.rxBytes = 0;
           sample.txBytes = 0;
           sample.rxTotalBytes = 0;
           sample.txTotalBytes = 0;
 
           self._saveStats(aTxn, aStore, sample);
         }
@@ -749,23 +817,23 @@ NetworkStatsDB.prototype = {
     }
 
     this._getCurrentStats(network, aResultCb);
   },
 
   _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) {
           result.rxBytes = result.rxTotalBytes = cursor.value.rxTotalBytes;
           result.txBytes = result.txTotalBytes = cursor.value.txTotalBytes;
         }
 
         txn.result = result;
@@ -773,21 +841,18 @@ NetworkStatsDB.prototype = {
     }.bind(this), aResultCb);
   },
 
   _getCurrentStatsFromDate: function _getCurrentStatsFromDate(aNetwork, aDate, aResultCb) {
     aDate = new Date(aDate);
     this.dbNewTxn(STATS_STORE_NAME, "readonly", function(txn, store) {
       let request = null;
       let start = this.normalizeDate(aDate);
-      let 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 };
 
       request = store.openCursor(range, "prev");
 
       request.onsuccess = function onsuccess(event) {
         let cursor = event.target.result;
         if (cursor) {
@@ -813,63 +878,103 @@ NetworkStatsDB.prototype = {
           }
 
           txn.result = result;
         };
       };
     }.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 = [];
 
       if (!aTxn.result) {
         aTxn.result = {};
       }
+      aTxn.result.appManifestURL = aAppManifestURL;
+      aTxn.result.browsingTrafficOnly = aBrowsingTrafficOnly;
+      aTxn.result.serviceType = aServiceType;
+      aTxn.result.network = aNetwork;
+      aTxn.result.start = aStart;
+      aTxn.result.end = aEnd;
 
       let request = aStore.openCursor(range).onsuccess = function(event) {
         var cursor = event.target.result;
         if (cursor){
           data.push({ rxBytes: cursor.value.rxBytes,
                       txBytes: cursor.value.txBytes,
                       date: new Date(cursor.value.timestamp + offset) });
           cursor.continue();
           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);
   },
 
   /*
    * Fill data array (samples from database) with empty samples to match
    * requested start / end dates.
    */
   fillResultSamples: function fillResultSamples(aStart, aEnd, aData) {
--- a/dom/network/NetworkStatsManager.js
+++ b/dom/network/NetworkStatsManager.js
@@ -67,16 +67,17 @@ NetworkStatsInterface.prototype = {
 // NetworkStats
 const NETWORKSTATS_CID = Components.ID("{28904f59-8497-4ac0-904f-2af14b7fd3de}");
 
 function NetworkStats(aWindow, aStats) {
   if (DEBUG) {
     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;
   this.end = aStats.end ? new aWindow.Date(aStats.end.getTime()) : null;
 
   let samples = this.data = new aWindow.Array();
   for (let i = 0; i < aStats.data.length; i++) {
     samples.push(aWindow.MozNetworkStatsData._create(
@@ -133,36 +134,46 @@ NetworkStatsManager.prototype = {
     if (aStart.constructor.name !== "Date" ||
         aEnd.constructor.name !== "Date" ||
         !(aNetwork instanceof this.window.MozNetworkStatsInterface) ||
         aStart > aEnd) {
       throw Components.results.NS_ERROR_INVALID_ARG;
     }
 
     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
     // This is just a work-around by passing timestamp numbers.
     aStart = aStart.getTime();
     aEnd = aEnd.getTime();
 
     let request = this.createRequest();
     cpmm.sendAsyncMessage("NetworkStats:Get",
                           { network: aNetwork.toJSON(),
                             start: aStart,
                             end: aEnd,
                             appManifestURL: appManifestURL,
+                            browsingTrafficOnly: browsingTrafficOnly,
                             serviceType: serviceType,
                             id: this.getRequestId(request) });
     return request;
   },
 
   clearStats: function clearStats(aNetwork) {
     this.checkPrivileges();
 
--- a/dom/network/NetworkStatsService.jsm
+++ b/dom/network/NetworkStatsService.jsm
@@ -410,40 +410,42 @@ this.NetworkStatsService = {
       if (!appId) {
         mm.sendAsyncMessage("NetworkStats:Get:Return",
                             { id: msg.id,
                               error: "Invalid appManifestURL", result: null });
         return;
       }
     }
 
+    let browsingTrafficOnly = msg.browsingTrafficOnly || false;
     let serviceType = msg.serviceType || "";
 
     let start = new Date(msg.start);
     let end = new Date(msg.end);
 
     let callback = (function (aError, aResult) {
       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) {
       if (!aNetId) {
         mm.sendAsyncMessage("NetworkStats:Get:Return",
                             { id: msg.id, error: "Invalid connectionType", result: null });
         return;
       }
 
       // If network is currently active we need to update the cached stats first before
       // retrieving stats from the DB.
       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) {
           this.updateCachedStats(callback);
           return;
         }
 
         this.updateStats(aNetId, function onStatsUpdated(aResult, aMessage) {
@@ -451,17 +453,17 @@ this.NetworkStatsService = {
         }.bind(this));
         return;
       }
 
       // Network not active, so no need to update
       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));
   },
 
   clearInterfaceStats: function clearInterfaceStats(mm, msg) {
     let self = this;
     let network = msg.network;
 
     debug("clear stats for network " + network.id + " of type " + network.type);
@@ -774,20 +776,21 @@ this.NetworkStatsService = {
    *
    */
   writeCache: function writeCache(aStats, aCallback) {
     debug("saveStats: " + aStats.appId + " " + aStats.isInBrowser + " " +
           aStats.serviceType + " " + aStats.networkId + " " +
           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|,
     // both |cachedStats| and |cachedStatsDate| will get updated.
     let diff = (this._db.normalizeDate(aStats.date) -
                 this._db.normalizeDate(this.cachedStatsDate)) /
                this._db.sampleRate;
     if (diff != 0) {