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 id28259
push userryanvm@gmail.com
push dateTue, 10 Feb 2015 20:32:50 +0000
treeherdermozilla-central@3d3f1b07ef0f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersalbert
bugs1070944
milestone38.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
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) {