Bug 951976 - Part 1: WebIDL implementation. r=gene
☠☠ backed out by 98deb29a6c1f ☠ ☠
authorBorting Chen <btchen@mozilla.com>
Mon, 10 Mar 2014 11:54:15 +0800
changeset 208757 b2afa18bea008c2b15ed85180dcf31be9cbeb0e0
parent 208756 610b1e0c7562af2c01260873a27c42ff7b51d65e
child 208758 5309d91dc590e170ad6f78016554cde8e50cbfc5
push id3857
push userraliiev@mozilla.com
push dateTue, 02 Sep 2014 16:39:23 +0000
treeherdermozilla-beta@5638b907b505 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersgene
bugs951976
milestone33.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 951976 - Part 1: WebIDL implementation. r=gene
b2g/app/b2g.js
b2g/chrome/content/shell.js
b2g/installer/package-manifest.in
dom/apps/src/PermissionsTable.jsm
dom/moz.build
dom/resourcestats/ResourceStatsControl.cpp
dom/resourcestats/ResourceStatsControl.h
dom/resourcestats/ResourceStatsDB.jsm
dom/resourcestats/ResourceStatsManager.js
dom/resourcestats/ResourceStatsManager.manifest
dom/resourcestats/ResourceStatsService.jsm
dom/resourcestats/moz.build
--- a/b2g/app/b2g.js
+++ b/b2g/app/b2g.js
@@ -476,16 +476,21 @@ pref("services.push.udp.wakeupEnabled", 
 pref("services.push.udp.well-known_netidAddress", "_wakeup_.");
 
 // NetworkStats
 #ifdef MOZ_WIDGET_GONK
 pref("dom.mozNetworkStats.enabled", true);
 pref("dom.webapps.firstRunWithSIM", true);
 #endif
 
+// ResourceStats
+#ifdef MOZ_WIDGET_GONK
+pref("dom.resource_stats.enabled", true);
+#endif
+
 #ifdef MOZ_B2G_RIL
 // SingleVariant
 pref("dom.mozApps.single_variant_sourcedir", "/persist/svoperapps");
 #endif
 
 // WebSettings
 pref("dom.mozSettings.enabled", true);
 pref("dom.navigator-property.disable.mozSettings", false);
--- a/b2g/chrome/content/shell.js
+++ b/b2g/chrome/content/shell.js
@@ -14,16 +14,17 @@ Cu.import('resource://gre/modules/Notifi
 Cu.import('resource://gre/modules/Payment.jsm');
 Cu.import("resource://gre/modules/AppsUtils.jsm");
 Cu.import('resource://gre/modules/UserAgentOverrides.jsm');
 Cu.import('resource://gre/modules/Keyboard.jsm');
 Cu.import('resource://gre/modules/ErrorPage.jsm');
 Cu.import('resource://gre/modules/AlertsHelper.jsm');
 #ifdef MOZ_WIDGET_GONK
 Cu.import('resource://gre/modules/NetworkStatsService.jsm');
+Cu.import('resource://gre/modules/ResourceStatsService.jsm');
 #endif
 
 // Identity
 Cu.import('resource://gre/modules/SignInToWebsite.jsm');
 SignInToWebsiteController.init();
 
 #ifdef MOZ_SERVICES_FXACCOUNTS
 Cu.import('resource://gre/modules/FxAccountsMgmtService.jsm');
--- a/b2g/installer/package-manifest.in
+++ b/b2g/installer/package-manifest.in
@@ -416,16 +416,24 @@
 @BINPATH@/components/NetworkStatsManager.js
 @BINPATH@/components/NetworkStatsManager.manifest
 @BINPATH@/components/NetworkStatsServiceProxy.js
 @BINPATH@/components/NetworkStatsServiceProxy.manifest
 @BINPATH@/components/WifiWorker.js
 @BINPATH@/components/WifiWorker.manifest
 #endif // MOZ_WIDGET_GONK
 
+; ResourceStats
+#ifdef MOZ_WIDGET_GONK
+@BINPATH@/components/ResourceStats.js
+@BINPATH@/components/ResourceStats.manifest
+@BINPATH@/components/ResourceStatsManager.js
+@BINPATH@/components/ResourceStatsManager.manifest
+#endif // MOZ_WIDGET_GONK
+
 ; RIL
 #if defined(MOZ_WIDGET_GONK) && defined(MOZ_B2G_RIL)
 @BINPATH@/components/MmsService.js
 @BINPATH@/components/MmsService.manifest
 @BINPATH@/components/MobileMessageDatabaseService.js
 @BINPATH@/components/MobileMessageDatabaseService.manifest
 @BINPATH@/components/RadioInterfaceLayer.js
 @BINPATH@/components/RadioInterfaceLayer.manifest
--- a/dom/apps/src/PermissionsTable.jsm
+++ b/dom/apps/src/PermissionsTable.jsm
@@ -181,16 +181,21 @@ this.PermissionsTable =  { geolocation: 
                              privileged: ALLOW_ACTION,
                              certified: ALLOW_ACTION
                            },
                            "networkstats-manage": {
                              app: DENY_ACTION,
                              privileged: DENY_ACTION,
                              certified: ALLOW_ACTION
                            },
+                           "resourcestats-manage": {
+                             app: DENY_ACTION,
+                             privileged: DENY_ACTION,
+                             certified: ALLOW_ACTION
+                           },
                            "wifi-manage": {
                              app: DENY_ACTION,
                              privileged: DENY_ACTION,
                              certified: ALLOW_ACTION
                            },
                            "systemXHR": {
                              app: DENY_ACTION,
                              privileged: ALLOW_ACTION,
--- a/dom/moz.build
+++ b/dom/moz.build
@@ -78,16 +78,17 @@ PARALLEL_DIRS += [
     'audiochannel',
     'promise',
     'smil',
     'telephony',
     'inputmethod',
     'webidl',
     'xbl',
     'xslt',
+    'resourcestats',
 ]
 
 if CONFIG['OS_ARCH'] == 'WINNT':
     PARALLEL_DIRS += ['plugins/ipc/hangui']
 
 if CONFIG['MOZ_WIDGET_TOOLKIT'] == 'gonk':
     PARALLEL_DIRS += [
         'speakermanager',
new file mode 100644
--- /dev/null
+++ b/dom/resourcestats/ResourceStatsControl.cpp
@@ -0,0 +1,39 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "ResourceStatsControl.h"
+#include "mozilla/Preferences.h"
+#include "nsIPermissionManager.h"
+#include "nsJSUtils.h"
+#include "nsPIDOMWindow.h"
+#include "nsServiceManagerUtils.h"
+
+using namespace mozilla::dom;
+
+/* static */ bool
+ResourceStatsControl::HasResourceStatsSupport(JSContext* /* unused */,
+                                              JSObject* aGlobal)
+{
+  // Disable the constructors if they're disabled by the preference for sure.
+  if (!Preferences::GetBool("dom.resource_stats.enabled", false)) {
+    return false;
+  }
+
+  // Get window.
+  nsCOMPtr<nsPIDOMWindow> win =
+    do_QueryInterface((nsISupports*)nsJSUtils::GetStaticScriptGlobal(aGlobal));
+  MOZ_ASSERT(!win || win->IsInnerWindow());
+  if (!win) {
+    return false;
+  }
+
+  // Check permission.
+  nsCOMPtr<nsIPermissionManager> permMgr =
+    do_GetService(NS_PERMISSIONMANAGER_CONTRACTID);
+  NS_ENSURE_TRUE(permMgr, false);
+
+  uint32_t permission = nsIPermissionManager::DENY_ACTION;
+  permMgr->TestPermissionFromWindow(win, "resourcestats-manage", &permission);
+  return permission == nsIPermissionManager::ALLOW_ACTION;
+}
new file mode 100644
--- /dev/null
+++ b/dom/resourcestats/ResourceStatsControl.h
@@ -0,0 +1,25 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef mozilla_dom_ResourceStatsConotrl_h
+#define mozilla_dom_ResourceStatsConotrl_h
+
+// Forward declarations.
+struct JSContext;
+class JSObject;
+
+namespace mozilla {
+namespace dom {
+
+class ResourceStatsControl
+{
+public:
+  static bool HasResourceStatsSupport(JSContext* /* unused */,
+                                      JSObject* aGlobal);
+};
+
+} // namespace dom
+} // namespace mozilla
+
+#endif // mozilla_dom_ResourceStatsConotrl_h
new file mode 100644
--- /dev/null
+++ b/dom/resourcestats/ResourceStatsDB.jsm
@@ -0,0 +1,537 @@
+/* This Source Code Form is subject to the terms of the Mozilla public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = ['ResourceStatsDB'];
+
+const DEBUG = false;
+function debug(s) { dump("-*- ResourceStatsDB: " + s + "\n"); }
+
+const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+Cu.import("resource://gre/modules/IndexedDBHelper.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyServiceGetter(this, "appsService",
+                                   "@mozilla.org/AppsService;1",
+                                   "nsIAppsService");
+
+const DB_NAME = "resource_stats";
+const DB_VERSION = 1;
+const POWER_STATS_STORE = "power_stats_store";
+const NETWORK_STATS_STORE = "network_stats_store";
+const ALARM_STORE = "alarm_store";
+
+const statsStoreNames = {
+  power: POWER_STATS_STORE,
+  network: NETWORK_STATS_STORE
+};
+
+// Constant defining the sampling rate.
+const SAMPLE_RATE = 24 * 60 * 60 * 1000; // 1 day.
+
+// Constant defining the MAX age of stored stats.
+const MAX_STORAGE_AGE = 180 * SAMPLE_RATE; // 180 days.
+
+this.ResourceStatsDB = function ResourceStatsDB() {
+  if (DEBUG) {
+    debug("Constructor()");
+  }
+
+  this.initDBHelper(DB_NAME, DB_VERSION,
+                    [POWER_STATS_STORE, NETWORK_STATS_STORE, ALARM_STORE]);
+};
+
+ResourceStatsDB.prototype = {
+  __proto__: IndexedDBHelper.prototype,
+
+  _dbNewTxn: function(aStoreName, aTxnType, aCallback, aTxnCb) {
+    function successCb(aResult) {
+      aTxnCb(null, aResult);
+    }
+    function errorCb(aError) {
+      aTxnCb(aError, null);
+    }
+    return this.newTxn(aTxnType, aStoreName, aCallback, successCb, errorCb);
+  },
+
+  upgradeSchema: function(aTransaction, aDb, aOldVersion, aNewVersion) {
+    if (DEBUG) {
+      debug("Upgrade DB from ver." + aOldVersion + " to ver." + aNewVersion);
+    }
+
+    let objectStore;
+
+    // Create PowerStatsStore.
+    objectStore = aDb.createObjectStore(POWER_STATS_STORE, {
+      keyPath: ["appId", "serviceType", "component", "timestamp"]
+    });
+    objectStore.createIndex("component", "component", { unique: false });
+
+    // Create NetworkStatsStore.
+    objectStore = aDb.createObjectStore(NETWORK_STATS_STORE, {
+      keyPath: ["appId", "serviceType", "component", "timestamp"]
+    });
+    objectStore.createIndex("component", "component", { unique: false });
+
+    // Create AlarmStore.
+    objectStore = aDb.createObjectStore(ALARM_STORE, {
+      keyPath: "alarmId",
+      autoIncrement: true
+    });
+    objectStore.createIndex("type", "type", { unique: false });
+    // Index for resource control target.
+    objectStore.createIndex("controlTarget",
+                            ["type", "manifestURL", "serviceType", "component"],
+                            { unique: false });
+  },
+
+  // Convert to UTC according to the current timezone and the filter timestamp
+  // to get SAMPLE_RATE precission.
+  _normalizeTime: function(aTime, aOffset) {
+    let time = Math.floor((aTime - aOffset) / SAMPLE_RATE) * SAMPLE_RATE;
+
+    return time;
+  },
+
+  /**
+   * aRecordArray contains an array of json objects storing network stats.
+   * The structure of the json object =
+   *  {
+   *    appId: XX,
+   *    serviceType: "XX",
+   *    componentStats: {
+   *      "component_1": { receivedBytes: XX, sentBytes: XX },
+   *      "component_2": { receivedBytes: XX, sentBytes: XX },
+   *      ...
+   *    },
+   *  }
+   */
+  saveNetworkStats: function(aRecordArray, aTimestamp, aResultCb) {
+    if (DEBUG) {
+      debug("saveNetworkStats()");
+    }
+
+    let offset = (new Date()).getTimezoneOffset() * 60 * 1000;
+    let timestamp = this._normalizeTime(aTimestamp, offset);
+
+    this._dbNewTxn(NETWORK_STATS_STORE, "readwrite", function(aTxn, aStore) {
+      aRecordArray.forEach(function(aRecord) {
+        let stats = {
+          appId: aRecord.appId,
+          serviceType: aRecord.serviceType,
+          component: "",
+          timestamp: timestamp,
+          receivedBytes: 0,
+          sentBytes: 0
+        };
+
+        let totalReceivedBytes = 0;
+        let totalSentBytes = 0;
+
+        // Save stats of each component.
+        let data = aRecord.componentStats;
+        for (let component in data) {
+          // Save stats to database.
+          stats.component = component;
+          stats.receivedBytes = data[component].receivedBytes;
+          stats.sentBytes = data[component].sentBytes;
+          aStore.put(stats);
+          if (DEBUG) {
+            debug("Save network stats: " + JSON.stringify(stats));
+          }
+
+          // Accumulated to tatal stats.
+          totalReceivedBytes += stats.receivedBytes;
+          totalSentBytes += stats.sentBytes;
+        }
+
+        // Save total stats.
+        stats.component = "";
+        stats.receivedBytes = totalReceivedBytes;
+        stats.sentBytes = totalSentBytes;
+        aStore.put(stats);
+        if (DEBUG) {
+          debug("Save network stats: " + JSON.stringify(stats));
+        }
+      });
+    }, aResultCb);
+  },
+
+  /**
+   * aRecordArray contains an array of json objects storing power stats.
+   * The structure of the json object =
+   *  {
+   *    appId: XX,
+   *    serviceType: "XX",
+   *    componentStats: {
+   *      "component_1": XX, // consumedPower
+   *      "component_2": XX,
+   *      ...
+   *    },
+   *  }
+   */
+  savePowerStats: function(aRecordArray, aTimestamp, aResultCb) {
+    if (DEBUG) {
+      debug("savePowerStats()");
+    }
+    let offset = (new Date()).getTimezoneOffset() * 60 * 1000;
+    let timestamp = this._normalizeTime(aTimestamp, offset);
+
+    this._dbNewTxn(POWER_STATS_STORE, "readwrite", function(aTxn, aStore) {
+      aRecordArray.forEach(function(aRecord) {
+        let stats = {
+          appId: aRecord.appId,
+          serviceType: aRecord.serviceType,
+          component: "",
+          timestamp: timestamp,
+          consumedPower: aRecord.totalStats
+        };
+
+        let totalConsumedPower = 0;
+
+        // Save stats of each component to database.
+        let data = aRecord.componentStats;
+        for (let component in data) {
+          // Save stats to database.
+          stats.component = component;
+          stats.consumedPower = data[component];
+          aStore.put(stats);
+          if (DEBUG) {
+            debug("Save power stats: " + JSON.stringify(stats));
+          }
+          // Accumulated to total stats.
+          totalConsumedPower += stats.consumedPower;
+        }
+
+        // Save total stats.
+        stats.component = "";
+        stats.consumedPower = totalConsumedPower;
+        aStore.put(stats);
+        if (DEBUG) {
+          debug("Save power stats: " + JSON.stringify(stats));
+        }
+      });
+    }, aResultCb);
+  },
+
+  // Get stats from a store.
+  getStats: function(aType, aManifestURL, aServiceType, aComponent,
+                     aStart, aEnd, aResultCb) {
+    if (DEBUG) {
+      debug(aType + "Mgr.getStats()");
+    }
+
+    let offset = (new Date()).getTimezoneOffset() * 60 * 1000;
+
+    // Get appId and check whether manifestURL is a valid app.
+    let appId = 0;
+    if (aManifestURL) {
+      appId = appsService.getAppLocalIdByManifestURL(aManifestURL);
+
+      if (!appId) {
+        aResultCb("Invalid manifestURL", null);
+        return;
+      }
+    }
+
+    // Get store name.
+    let storeName = statsStoreNames[aType];
+
+    // Normalize start time and end time to SAMPLE_RATE precission.
+    let start = this._normalizeTime(aStart, offset);
+    let end = this._normalizeTime(aEnd, offset);
+    if (DEBUG) {
+      debug("Query time range: " + start + " to " + end);
+      debug("[appId, serviceType, component] = [" + appId + ", " + aServiceType
+            + ", " + aComponent + "]");
+    }
+
+    // Create filters.
+    let lowerFilter = [appId, aServiceType, aComponent, start];
+    let upperFilter = [appId, aServiceType, aComponent, end];
+
+    // Execute DB query.
+    this._dbNewTxn(storeName, "readonly", function(aTxn, aStore) {
+      let range = IDBKeyRange.bound(lowerFilter, upperFilter, false, false);
+
+      let statsData = [];
+
+      if (!aTxn.result) {
+        aTxn.result = Object.create(null);
+      }
+      aTxn.result.type = aType;
+      aTxn.result.component = aComponent;
+      aTxn.result.serviceType = aServiceType;
+      aTxn.result.manifestURL = aManifestURL;
+      aTxn.result.start = start + offset;
+      aTxn.result.end = end + offset;
+      // Since ResourceStats() would require SAMPLE_RATE when filling the empty
+      // entries of statsData array, we append SAMPLE_RATE to the result field
+      // to save an IPC call.
+      aTxn.result.sampleRate = SAMPLE_RATE;
+
+      let request = aStore.openCursor(range, "prev");
+      if (aType == "power") {
+        request.onsuccess = function(aEvent) {
+          var cursor = aEvent.target.result;
+          if (cursor) {
+            if (DEBUG) {
+              debug("Get " + JSON.stringify(cursor.value));
+            }
+
+            // Covert timestamp to current timezone.
+            statsData.push({
+              timestamp: cursor.value.timestamp + offset,
+              consumedPower: cursor.value.consumedPower
+            });
+            cursor.continue();
+            return;
+          }
+          aTxn.result.statsData = statsData;
+        };
+      } else if (aType == "network") {
+        request.onsuccess = function(aEvent) {
+          var cursor = aEvent.target.result;
+          if (cursor) {
+            if (DEBUG) {
+              debug("Get " + JSON.stringify(cursor.value));
+            }
+
+            // Covert timestamp to current timezone.
+            statsData.push({
+              timestamp: cursor.value.timestamp + offset,
+              receivedBytes: cursor.value.receivedBytes,
+              sentBytes: cursor.value.sentBytes
+            });
+            cursor.continue();
+            return;
+          }
+          aTxn.result.statsData = statsData;
+        };
+      }
+    }, aResultCb);
+  },
+
+  // Delete the stats of a specific app/service (within a specified time range).
+  clearStats: function(aType, aAppId, aServiceType, aComponent,
+                       aStart, aEnd, aResultCb) {
+    if (DEBUG) {
+      debug(aType + "Mgr.clearStats()");
+    }
+
+    let offset = (new Date()).getTimezoneOffset() * 60 * 1000;
+
+    // Get store name.
+    let storeName = statsStoreNames[aType];
+
+    // Normalize start and end time to SAMPLE_RATE precission.
+    let start = this._normalizeTime(aStart, offset);
+    let end = this._normalizeTime(aEnd, offset);
+    if (DEBUG) {
+      debug("Query time range: " + start + " to " + end);
+      debug("[appId, serviceType, component] = [" + aAppId + ", " + aServiceType
+            + ", " + aComponent + "]");
+    }
+
+    // Create filters.
+    let lowerFilter = [aAppId, aServiceType, aComponent, start];
+    let upperFilter = [aAppId, aServiceType, aComponent, end];
+
+    // Execute clear operation.
+    this._dbNewTxn(storeName, "readwrite", function(aTxn, aStore) {
+      let range = IDBKeyRange.bound(lowerFilter, upperFilter, false, false);
+      let request = aStore.openCursor(range).onsuccess = function(aEvent) {
+        let cursor = aEvent.target.result;
+        if (cursor) {
+          if (DEBUG) {
+            debug("Delete " + JSON.stringify(cursor.value) + " from database");
+          }
+          cursor.delete();
+          cursor.continue();
+          return;
+        }
+      };
+    }, aResultCb);
+  },
+
+  // Delete all stats saved in a store.
+  clearAllStats: function(aType, aResultCb) {
+    if (DEBUG) {
+      debug(aType + "Mgr.clearAllStats()");
+    }
+
+    let storeName = statsStoreNames[aType];
+
+    // Execute clear operation.
+    this._dbNewTxn(storeName, "readwrite", function(aTxn, aStore) {
+      if (DEBUG) {
+        debug("Clear " + aType + " stats from datastore");
+      }
+      aStore.clear();
+    }, aResultCb);
+  },
+
+  addAlarm: function(aAlarm, aResultCb) {
+    if (DEBUG) {
+      debug(aAlarm.type + "Mgr.addAlarm()");
+      debug("alarm = " + JSON.stringify(aAlarm));
+    }
+
+    this._dbNewTxn(ALARM_STORE, "readwrite", function(aTxn, aStore) {
+      aStore.put(aAlarm).onsuccess = function setResult(aEvent) {
+        // Get alarmId.
+        aTxn.result = aEvent.target.result;
+        if (DEBUG) {
+          debug("New alarm ID: " + aTxn.result);
+        }
+      };
+    }, aResultCb);
+  },
+
+  // Convert DB record to alarm object.
+  _recordToAlarm: function(aRecord) {
+    let alarm = {
+      alarmId: aRecord.alarmId,
+      type: aRecord.type,
+      component: aRecord.component,
+      serviceType: aRecord.serviceType,
+      manifestURL: aRecord.manifestURL,
+      threshold: aRecord.threshold,
+      data: aRecord.data
+    };
+
+    return alarm;
+  },
+
+  getAlarms: function(aType, aOptions, aResultCb) {
+    if (DEBUG) {
+      debug(aType + "Mgr.getAlarms()");
+      debug("[appId, serviceType, component] = [" + aOptions.appId + ", "
+            + aOptions.serviceType + ", " + aOptions.component + "]");
+    }
+
+    // Execute clear operation.
+    this._dbNewTxn(ALARM_STORE, "readwrite", function(aTxn, aStore) {
+      if (!aTxn.result) {
+        aTxn.result = [];
+      }
+
+      let indexName = null;
+      let range = null;
+
+      if (aOptions) { // Get alarms associated to specified statsOptions.
+        indexName = "controlTarget";
+        range = IDBKeyRange.only([aType, aOptions.manifestURL,
+                                  aOptions.serviceType, aOptions.component]);
+      } else { // Get all alarms of the specified type.
+        indexName = "type";
+        range = IDBKeyRange.only(aType);
+      }
+
+      let request = aStore.index(indexName).openCursor(range);
+      request.onsuccess = function onsuccess(aEvent) {
+        let cursor = aEvent.target.result;
+        if (cursor) {
+          aTxn.result.push(this._recordToAlarm(cursor.value));
+          cursor.continue();
+          return;
+        }
+      }.bind(this);
+    }.bind(this), aResultCb);
+  },
+
+  removeAlarm: function(aType, aAlarmId, aResultCb) {
+    if (DEBUG) {
+      debug("removeAlarms(" + aAlarmId + ")");
+    }
+
+    // Execute clear operation.
+    this._dbNewTxn(ALARM_STORE, "readwrite", function(aTxn, aStore) {
+      aStore.get(aAlarmId).onsuccess = function onsuccess(aEvent) {
+        let alarm = aEvent.target.result;
+        aTxn.result = false;
+        if (!alarm || aType !== alarm.type) {
+          return;
+        }
+
+        if (DEBUG) {
+          debug("Remove alarm " + JSON.stringify(alarm) + " from datastore");
+        }
+        aStore.delete(aAlarmId);
+        aTxn.result = true;
+      };
+    }, aResultCb);
+  },
+
+  removeAllAlarms: function(aType, aResultCb) {
+    if (DEBUG) {
+      debug(aType + "Mgr.removeAllAlarms()");
+    }
+
+    // Execute clear operation.
+    this._dbNewTxn(ALARM_STORE, "readwrite", function(aTxn, aStore) {
+      if (DEBUG) {
+        debug("Remove all " + aType + " alarms from datastore.");
+      }
+
+      let range = IDBKeyRange.only(aType);
+      let request = aStore.index("type").openCursor(range);
+      request.onsuccess = function onsuccess(aEvent) {
+        let cursor = aEvent.target.result;
+        if (cursor) {
+          if (DEBUG) {
+            debug("Remove " + JSON.stringify(cursor.value) + " from database.");
+          }
+          cursor.delete();
+          cursor.continue();
+          return;
+        }
+      };
+    }, aResultCb);
+  },
+
+  // Get all index keys of the component.
+  getComponents: function(aType, aResultCb) {
+    if (DEBUG) {
+      debug(aType + "Mgr.getComponents()");
+    }
+
+    let storeName = statsStoreNames[aType];
+
+    this._dbNewTxn(storeName, "readonly", function(aTxn, aStore) {
+      if (!aTxn.result) {
+        aTxn.result = [];
+      }
+
+      let request = aStore.index("component").openKeyCursor(null, "nextunique");
+      request.onsuccess = function onsuccess(aEvent) {
+        let cursor = aEvent.target.result;
+        if (cursor) {
+          aTxn.result.push(cursor.key);
+          cursor.continue();
+          return;
+        }
+
+        // Remove "" from the result, which indicates sum of all
+        // components' stats.
+        let index = aTxn.result.indexOf("");
+        if (index > -1) {
+          aTxn.result.splice(index, 1);
+        }
+      };
+    }, aResultCb);
+  },
+
+  get sampleRate () {
+    return SAMPLE_RATE;
+  },
+
+  get maxStorageAge() {
+    return MAX_STORAGE_AGE;
+  },
+};
+
new file mode 100644
--- /dev/null
+++ b/dom/resourcestats/ResourceStatsManager.js
@@ -0,0 +1,480 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+"use strict";
+
+const DEBUG = false;
+function debug(s) { dump("-*- ResourceStatsManager: " + 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/DOMRequestHelper.jsm");
+
+// Constant defines supported statistics.
+const resourceTypeList = ["network", "power"];
+
+function NetworkStatsData(aStatsData) {
+  if (DEBUG) {
+    debug("NetworkStatsData(): " + JSON.stringify(aStatsData));
+  }
+
+  this.receivedBytes = aStatsData.receivedBytes || 0;
+  this.sentBytes = aStatsData.sentBytes || 0;
+  this.timestamp = aStatsData.timestamp;
+}
+
+NetworkStatsData.prototype = {
+  classID: Components.ID("{dce5729a-ba92-4185-8854-e29e71b9e8a2}"),
+  contractID: "@mozilla.org/networkStatsData;1",
+  QueryInterface: XPCOMUtils.generateQI([])
+};
+
+function PowerStatsData(aStatsData) {
+  if (DEBUG) {
+    debug("PowerStatsData(): " + JSON.stringify(aStatsData));
+  }
+
+  this.consumedPower = aStatsData.consumedPower || 0;
+  this.timestamp = aStatsData.timestamp;
+}
+
+PowerStatsData.prototype = {
+  classID: Components.ID("{acb9af6c-8143-4e59-bc18-4bb1736a4004}"),
+  contractID: "@mozilla.org/powerStatsData;1",
+  QueryInterface: XPCOMUtils.generateQI([])
+};
+
+function ResourceStats(aWindow, aStats) {
+  if (DEBUG) {
+    debug("ResourceStats(): " + JSON.stringify(aStats));
+  }
+
+  this._window = aWindow;
+  this.type = aStats.type;
+  this.component = aStats.component || null;
+  this.serviceType = aStats.serviceType || null;
+  this.manifestURL = aStats.manifestURL || null;
+  this.start = aStats.start;
+  this.end = aStats.end;
+  this.statsData = new aWindow.Array();
+
+  // A function creates a StatsData object according to type.
+  let createStatsDataObject = null;
+  let self = this;
+  switch (this.type) {
+    case "power":
+      createStatsDataObject = function(aStats) {
+        let chromeObj = new PowerStatsData(aStats);
+        return self._window.PowerStatsData._create(self._window, chromeObj);
+      };
+      break;
+    case "network":
+      createStatsDataObject = function(aStats) {
+        let chromeObj = new NetworkStatsData(aStats);
+        return self._window.NetworkStatsData._create(self._window, chromeObj);
+      };
+      break;
+  }
+
+  let sampleRate = aStats.sampleRate;
+  let queryResult = aStats.statsData;
+  let stats = queryResult.pop(); // Pop out the last element.
+  let timestamp = this.start;
+
+  // Push query result to statsData, and fill empty elements so that:
+  // 1. the timestamp of the first element of statsData is equal to start;
+  // 2. the timestamp of the last element of statsData is equal to end;
+  // 3. the timestamp difference of every neighboring elements is SAMPLE_RATE.
+  for (; timestamp <= this.end; timestamp += sampleRate) {
+    if (!stats || stats.timestamp != timestamp) {
+      // If dataArray is empty or timestamp are not equal, push a dummy object
+      // (which stats are set to 0) to statsData.
+      this.statsData.push(createStatsDataObject({ timestamp: timestamp }));
+    } else {
+      // Push stats to statsData and pop a new element form queryResult.
+      this.statsData.push(createStatsDataObject(stats));
+      stats = queryResult.pop();
+    }
+  }
+}
+
+ResourceStats.prototype = {
+  getData: function() {
+    return this.statsData;
+  },
+
+  classID: Components.ID("{b7c970f2-3d58-4966-9633-2024feb5132b}"),
+  contractID: "@mozilla.org/resourceStats;1",
+  QueryInterface: XPCOMUtils.generateQI([])
+};
+
+function ResourceStatsAlarm(aWindow, aAlarm) {
+  if (DEBUG) {
+    debug("ResourceStatsAlarm(): " + JSON.stringify(aAlarm));
+  }
+
+  this.alarmId = aAlarm.alarmId;
+  this.type = aAlarm.type;
+  this.component = aAlarm.component || null;
+  this.serviceType = aAlarm.serviceType || null;
+  this.manifestURL = aAlarm.manifestURL || null;
+  this.threshold = aAlarm.threshold;
+
+  // Clone data object using structured clone algorithm.
+  this.data = null;
+  if (aAlarm.data) {
+    this.data = Cu.cloneInto(aAlarm.data, aWindow);
+  }
+}
+
+ResourceStatsAlarm.prototype = {
+  classID: Components.ID("{e2b66e7a-0ff1-4015-8690-a2a3f6a5b63a}"),
+  contractID: "@mozilla.org/resourceStatsAlarm;1",
+  QueryInterface: XPCOMUtils.generateQI([]),
+};
+
+function ResourceStatsManager() {
+  if (DEBUG) {
+    debug("constructor()");
+  }
+}
+
+ResourceStatsManager.prototype = {
+  __proto__: DOMRequestIpcHelper.prototype,
+
+  _getPromise: function(aCallback) {
+    let self = this;
+    return this.createPromise(function(aResolve, aReject) {
+      let resolverId = self.getPromiseResolverId({
+        resolve: aResolve,
+        reject: aReject
+      });
+
+      aCallback(resolverId);
+    });
+  },
+
+  // Check time range.
+  _checkTimeRange: function(aStart, aEnd) {
+    if (DEBUG) {
+      debug("_checkTimeRange(): " + aStart + " to " + aEnd);
+    }
+
+    let start = aStart ? aStart : 0;
+    let end = aEnd ? aEnd : Date.now();
+
+    if (start > end) {
+      throw Cr.NS_ERROR_INVALID_ARG;
+    }
+
+    return { start: start, end: end };
+  },
+
+  getStats: function(aStatsOptions, aStart, aEnd) {
+    // Check time range.
+    let { start: start, end: end } = this._checkTimeRange(aStart, aEnd);
+
+    // Create Promise.
+    let self = this;
+    return this._getPromise(function(aResolverId) {
+      self.cpmm.sendAsyncMessage("ResourceStats:GetStats", {
+        resolverId: aResolverId,
+        type: self.type,
+        statsOptions: aStatsOptions,
+        manifestURL: self._manifestURL,
+        start: start,
+        end: end
+      });
+    });
+  },
+
+  clearStats: function(aStatsOptions, aStart, aEnd) {
+    // Check time range.
+    let {start: start, end: end} = this._checkTimeRange(aStart, aEnd);
+
+    // Create Promise.
+    let self = this;
+    return this._getPromise(function(aResolverId) {
+      self.cpmm.sendAsyncMessage("ResourceStats:ClearStats", {
+        resolverId: aResolverId,
+        type: self.type,
+        statsOptions: aStatsOptions,
+        manifestURL: self._manifestURL,
+        start: start,
+        end: end
+      });
+    });
+  },
+
+  clearAllStats: function() {
+    // Create Promise.
+    let self = this;
+    return this._getPromise(function(aResolverId) {
+      self.cpmm.sendAsyncMessage("ResourceStats:ClearAllStats", {
+        resolverId: aResolverId,
+        type: self.type,
+        manifestURL: self._manifestURL
+      });
+    });
+  },
+
+  addAlarm: function(aThreshold, aStatsOptions, aAlarmOptions) {
+    if (DEBUG) {
+      debug("aStatsOptions: " + JSON.stringify(aAlarmOptions));
+      debug("aAlarmOptions: " + JSON.stringify(aAlarmOptions));
+    }
+
+    // Parse alarm options.
+    let startTime = aAlarmOptions.startTime || 0;
+
+    // Clone data object using structured clone algorithm.
+    let data = null;
+    if (aAlarmOptions.data) {
+      data = Cu.cloneInto(aAlarmOptions.data, this._window);
+    }
+
+    // Create Promise.
+    let self = this;
+    return this._getPromise(function(aResolverId) {
+      self.cpmm.sendAsyncMessage("ResourceStats:AddAlarm", {
+        resolverId: aResolverId,
+        type: self.type,
+        threshold: aThreshold,
+        statsOptions: aStatsOptions,
+        manifestURL: self._manifestURL,
+        startTime: startTime,
+        data: data
+      });
+    });
+  },
+
+  getAlarms: function(aStatsOptions) {
+    // Create Promise.
+    let self = this;
+    return this._getPromise(function(aResolverId) {
+      self.cpmm.sendAsyncMessage("ResourceStats:GetAlarms", {
+        resolverId: aResolverId,
+        type: self.type,
+        statsOptions: aStatsOptions,
+        manifestURL: self._manifestURL
+      });
+    });
+  },
+
+  removeAlarm: function(aAlarmId) {
+    // Create Promise.
+    let self = this;
+    return this._getPromise(function(aResolverId) {
+      self.cpmm.sendAsyncMessage("ResourceStats:RemoveAlarm", {
+        resolverId: aResolverId,
+        type: self.type,
+        manifestURL: self._manifestURL,
+        alarmId: aAlarmId
+      });
+    });
+  },
+
+  removeAllAlarms: function() {
+    // Create Promise.
+    let self = this;
+    return this._getPromise(function(aResolverId) {
+      self.cpmm.sendAsyncMessage("ResourceStats:RemoveAllAlarms", {
+        resolverId: aResolverId,
+        type: self.type,
+        manifestURL: self._manifestURL
+      });
+    });
+  },
+
+  getAvailableComponents: function() {
+    // Create Promise.
+    let self = this;
+    return this._getPromise(function(aResolverId) {
+      self.cpmm.sendAsyncMessage("ResourceStats:GetComponents", {
+        resolverId: aResolverId,
+        type: self.type,
+        manifestURL: self._manifestURL
+      });
+    });
+  },
+
+  get resourceTypes() {
+    let types = new this._window.Array();
+    resourceTypeList.forEach(function(aType) {
+      types.push(aType);
+    });
+
+    return types;
+  },
+
+  get sampleRate() {
+    let msg = { manifestURL: this._manifestURL };
+
+    return this.cpmm.sendSyncMessage("ResourceStats:SampleRate", msg)[0];
+  },
+
+  get maxStorageAge() {
+    let msg = { manifestURL: this._manifestURL };
+
+    return this.cpmm.sendSyncMessage("ResourceStats:MaxStorageAge", msg)[0];
+  },
+
+  receiveMessage: function(aMessage) {
+    let data = aMessage.data;
+    let chromeObj = null;
+    let webidlObj = null;
+    let self = this;
+
+    if (DEBUG) {
+      debug("receiveMessage(): " + aMessage.name + " " + data.resolverId);
+    }
+
+    let resolver = this.takePromiseResolver(data.resolverId);
+    if (!resolver) {
+      return;
+    }
+
+    switch (aMessage.name) {
+      case "ResourceStats:GetStats:Resolved":
+        if (DEBUG) {
+          debug("data.value = " + JSON.stringify(data.value));
+        }
+
+        chromeObj = new ResourceStats(this._window, data.value);
+        webidlObj = this._window.ResourceStats._create(this._window, chromeObj);
+        resolver.resolve(webidlObj);
+        break;
+
+      case "ResourceStats:AddAlarm:Resolved":
+        if (DEBUG) {
+          debug("data.value = " + JSON.stringify(data.value));
+        }
+
+        resolver.resolve(data.value); // data.value is alarmId.
+        break;
+
+      case "ResourceStats:GetAlarms:Resolved":
+        if (DEBUG) {
+          debug("data.value = " + JSON.stringify(data.value));
+        }
+
+        let alarmArray = this._window.Array();
+        data.value.forEach(function(aAlarm) {
+          chromeObj = new ResourceStatsAlarm(self._window, aAlarm);
+          webidlObj = self._window.ResourceStatsAlarm._create(self._window,
+                                                              chromeObj);
+          alarmArray.push(webidlObj);
+        });
+        resolver.resolve(alarmArray);
+        break;
+
+      case "ResourceStats:GetComponents:Resolved":
+        if (DEBUG) {
+          debug("data.value = " + JSON.stringify(data.value));
+        }
+
+        let components = this._window.Array();
+        data.value.forEach(function(aComponent) {
+          components.push(aComponent);
+        });
+        resolver.resolve(components);
+        break;
+
+      case "ResourceStats:ClearStats:Resolved":
+      case "ResourceStats:ClearAllStats:Resolved":
+      case "ResourceStats:RemoveAlarm:Resolved":
+      case "ResourceStats:RemoveAllAlarms:Resolved":
+        if (DEBUG) {
+          debug("data.value = " + JSON.stringify(data.value));
+        }
+
+        resolver.resolve(data.value);
+        break;
+
+      case "ResourceStats:GetStats:Rejected":
+      case "ResourceStats:ClearStats:Rejected":
+      case "ResourceStats:ClearAllStats:Rejected":
+      case "ResourceStats:AddAlarm:Rejected":
+      case "ResourceStats:GetAlarms:Rejected":
+      case "ResourceStats:RemoveAlarm:Rejected":
+      case "ResourceStats:RemoveAllAlarms:Rejected":
+      case "ResourceStats:GetComponents:Rejected":
+        if (DEBUG) {
+          debug("data.reason = " + JSON.stringify(data.reason));
+        }
+
+        resolver.reject(data.reason);
+        break;
+
+      default:
+        if (DEBUG) {
+          debug("Could not find a handler for " + aMessage.name);
+        }
+        resolver.reject();
+    }
+  },
+
+  __init: function(aType) {
+    if (resourceTypeList.indexOf(aType) < 0) {
+      if (DEBUG) {
+        debug("Do not support resource statistics for " + aType);
+      }
+      throw Cr.NS_ERROR_INVALID_ARG;
+    } else {
+      if (DEBUG) {
+        debug("Create " + aType + "Mgr");
+      }
+      this.type = aType;
+    }
+  },
+
+  init: function(aWindow) {
+    if (DEBUG) {
+      debug("init()");
+    }
+
+    // Get the manifestURL if this is an installed app
+    let principal = aWindow.document.nodePrincipal;
+    let appsService = Cc["@mozilla.org/AppsService;1"]
+                        .getService(Ci.nsIAppsService);
+    this._manifestURL = appsService.getManifestURLByLocalId(principal.appId);
+
+    const messages = ["ResourceStats:GetStats:Resolved",
+                      "ResourceStats:ClearStats:Resolved",
+                      "ResourceStats:ClearAllStats:Resolved",
+                      "ResourceStats:AddAlarm:Resolved",
+                      "ResourceStats:GetAlarms:Resolved",
+                      "ResourceStats:RemoveAlarm:Resolved",
+                      "ResourceStats:RemoveAllAlarms:Resolved",
+                      "ResourceStats:GetComponents:Resolved",
+                      "ResourceStats:GetStats:Rejected",
+                      "ResourceStats:ClearStats:Rejected",
+                      "ResourceStats:ClearAllStats:Rejected",
+                      "ResourceStats:AddAlarm:Rejected",
+                      "ResourceStats:GetAlarms:Rejected",
+                      "ResourceStats:RemoveAlarm:Rejected",
+                      "ResourceStats:RemoveAllAlarms:Rejected",
+                      "ResourceStats:GetComponents:Rejected"];
+    this.initDOMRequestHelper(aWindow, messages);
+
+    this.cpmm = Cc["@mozilla.org/childprocessmessagemanager;1"]
+                  .getService(Ci.nsISyncMessageSender);
+  },
+
+  classID: Components.ID("{101ed1f8-31b3-491c-95ea-04091e6e8027}"),
+  contractID: "@mozilla.org/resourceStatsManager;1",
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIDOMGlobalPropertyInitializer,
+                                         Ci.nsISupportsWeakReference,
+                                         Ci.nsIObserver])
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([NetworkStatsData,
+                                                     PowerStatsData,
+                                                     ResourceStats,
+                                                     ResourceStatsAlarm,
+                                                     ResourceStatsManager]);
+
new file mode 100644
--- /dev/null
+++ b/dom/resourcestats/ResourceStatsManager.manifest
@@ -0,0 +1,14 @@
+component {dce5729a-ba92-4185-8854-e29e71b9e8a2} ResourceStatsManager.js
+contract @mozilla.org/networkStatsData;1 {dce5729a-ba92-4185-8854-e29e71b9e8a2}
+
+component {acb9af6c-8143-4e59-bc18-4bb1736a4004} ResourceStatsManager.js
+contract @mozilla.org/powerStatsData;1 {acb9af6c-8143-4e59-bc18-4bb1736a4004}
+
+component {b7c970f2-3d58-4966-9633-2024feb5132b} ResourceStatsManager.js
+contract @mozilla.org/resourceStats;1 {b7c970f2-3d58-4966-9633-2024feb5132b}
+
+component {e2b66e7a-0ff1-4015-8690-a2a3f6a5b63a} ResourceStatsManager.js
+contract @mozilla.org/resourceStatsAlarm;1 {e2b66e7a-0ff1-4015-8690-a2a3f6a5b63a}
+
+component {101ed1f8-31b3-491c-95ea-04091e6e8027} ResourceStatsManager.js
+contract @mozilla.org/resourceStatsManager;1 {101ed1f8-31b3-491c-95ea-04091e6e8027}
new file mode 100644
--- /dev/null
+++ b/dom/resourcestats/ResourceStatsService.jsm
@@ -0,0 +1,334 @@
+/* This Source Code Form is subject to the terms of the Mozilla public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = ["ResourceStatsService"];
+
+const DEBUG = false;
+function debug(s) { dump("-*- ResourceStatsService: " + 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");
+// Load ResourceStatsDB.
+Cu.import("resource://gre/modules/ResourceStatsDB.jsm");
+
+XPCOMUtils.defineLazyServiceGetter(this, "gIDBManager",
+                                   "@mozilla.org/dom/indexeddb/manager;1",
+                                   "nsIIndexedDatabaseManager");
+
+XPCOMUtils.defineLazyServiceGetter(this, "ppmm",
+                                   "@mozilla.org/parentprocessmessagemanager;1",
+                                   "nsIMessageListenerManager");
+
+XPCOMUtils.defineLazyServiceGetter(this, "appsService",
+                                   "@mozilla.org/AppsService;1",
+                                   "nsIAppsService");
+
+this.ResourceStatsService = {
+
+  init: function() {
+    if (DEBUG) {
+      debug("Service started");
+    }
+
+    // Set notification to observe.
+    Services.obs.addObserver(this, "xpcom-shutdown", false);
+
+    // Add message listener.
+    this.messages = ["ResourceStats:GetStats",
+                     "ResourceStats:ClearStats",
+                     "ResourceStats:ClearAllStats",
+                     "ResourceStats:AddAlarm",
+                     "ResourceStats:GetAlarms",
+                     "ResourceStats:RemoveAlarm",
+                     "ResourceStats:RemoveAllAlarms",
+                     "ResourceStats:GetComponents",
+                     "ResourceStats:SampleRate",
+                     "ResourceStats:MaxStorageAge"];
+
+    this.messages.forEach(function(aMessageName){
+      ppmm.addMessageListener(aMessageName, this);
+    }, this);
+
+    // Create indexedDB.
+    this._db = new ResourceStatsDB();
+  },
+
+  receiveMessage: function(aMessage) {
+    if (DEBUG) {
+      debug("receiveMessage(): " + aMessage.name);
+    }
+
+    let mm = aMessage.target;
+    let data = aMessage.data;
+
+    if (DEBUG) {
+      debug("received aMessage.data = " + JSON.stringify(data));
+    }
+
+    // To prevent the hacked child process from sending commands to parent,
+    // we need to check its permission and manifest URL.
+    if (!aMessage.target.assertPermission("resourcestats-manage")) {
+      return;
+    }
+    if (!aMessage.target.assertContainApp(data.manifestURL)) {
+      if (DEBUG) {
+        debug("Got msg from a child process containing illegal manifestURL.");
+      }
+      return;
+    }
+
+    switch (aMessage.name) {
+      case "ResourceStats:GetStats":
+        this.getStats(mm, data);
+        break;
+      case "ResourceStats:ClearStats":
+        this.clearStats(mm, data);
+        break;
+      case "ResourceStats:ClearAllStats":
+        this.clearAllStats(mm, data);
+        break;
+      case "ResourceStats:AddAlarm":
+        this.addAlarm(mm, data);
+        break;
+      case "ResourceStats:GetAlarms":
+        this.getAlarms(mm, data);
+        break;
+      case "ResourceStats:RemoveAlarm":
+        this.removeAlarm(mm, data);
+        break;
+      case "ResourceStats:RemoveAllAlarms":
+        this.removeAllAlarms(mm, data);
+        break;
+      case "ResourceStats:GetComponents":
+        this.getComponents(mm, data);
+        break;
+      case "ResourceStats:SampleRate":
+        // This message is sync.
+        return this._db.sampleRate;
+      case "ResourceStats:MaxStorageAge":
+        // This message is sync.
+        return this._db.maxStorageAge;
+    }
+  },
+
+  observe: function(aSubject, aTopic, aData) {
+    switch (aTopic) {
+      case "xpcom-shutdown":
+        if (DEBUG) {
+          debug("Service shutdown " + aData);
+        }
+
+        this.messages.forEach(function(aMessageName) {
+          ppmm.removeMessageListener(aMessageName, this);
+        }, this);
+
+        Services.obs.removeObserver(this, "xpcom-shutdown");
+        break;
+      default:
+        return;
+    }
+  },
+
+  // Closure generates callback function for DB request.
+  _createDbCallback: function(aMm, aId, aMessage) {
+    let resolveMsg = aMessage + ":Resolved";
+    let rejectMsg = aMessage + ":Rejected";
+
+    return function(aError, aResult) {
+      if (aError) {
+        aMm.sendAsyncMessage(rejectMsg, { resolverId: aId, reason: aError });
+        return;
+      }
+      aMm.sendAsyncMessage(resolveMsg, { resolverId: aId, value: aResult });
+    };
+  },
+
+  getStats: function(aMm, aData) {
+    if (DEBUG) {
+      debug("getStats(): " + JSON.stringify(aData));
+    }
+
+    // Note: we validate the manifestURL in _db.getStats().
+    let manifestURL = aData.statsOptions.manifestURL || "";
+    let serviceType = aData.statsOptions.serviceType || "";
+    let component = aData.statsOptions.component || "";
+
+    // Execute DB operation.
+    let onStatsGot = this._createDbCallback(aMm, aData.resolverId,
+                                            "ResourceStats:GetStats");
+    this._db.getStats(aData.type, manifestURL, serviceType, component,
+                      aData.start, aData.end, onStatsGot);
+  },
+
+  clearStats: function(aMm, aData) {
+    if (DEBUG) {
+      debug("clearStats(): " + JSON.stringify(aData));
+    }
+
+    // Get appId and check whether manifestURL is a valid app.
+    let appId = 0;
+    let manifestURL = aData.statsOptions.manifestURL || "";
+    if (manifestURL) {
+      appId = appsService.getAppLocalIdByManifestURL(manifestURL);
+
+      if (!appId) {
+        aMm.sendAsyncMessage("ResourceStats:GetStats:Rejected", {
+          resolverId: aData.resolverId,
+          reason: "Invalid manifestURL"
+        });
+        return;
+      }
+    }
+
+    let serviceType = aData.statsOptions.serviceType || "";
+    let component = aData.statsOptions.component || "";
+
+    // Execute DB operation.
+    let onStatsCleared = this._createDbCallback(aMm, aData.resolverId,
+                                                "ResourceStats:ClearStats");
+    this._db.clearStats(aData.type, appId, serviceType, component,
+                        aData.start, aData.end, onStatsCleared);
+  },
+
+  clearAllStats: function(aMm, aData) {
+    if (DEBUG) {
+      debug("clearAllStats(): " + JSON.stringify(aData));
+    }
+
+    // Execute DB operation.
+    let onAllStatsCleared = this._createDbCallback(aMm, aData.resolverId,
+                                                   "ResourceStats:ClearAllStats");
+    this._db.clearAllStats(aData.type, onAllStatsCleared);
+  },
+
+  addAlarm: function(aMm, aData) {
+    if (DEBUG) {
+      debug("addAlarm(): " + JSON.stringify(aData));
+    }
+
+    // Get appId and check whether manifestURL is a valid app.
+    let manifestURL = aData.statsOptions.manifestURL;
+    if (manifestURL) {
+      let appId = appsService.getAppLocalIdByManifestURL(manifestURL);
+
+      if (!appId) {
+        aMm.sendAsyncMessage("ResourceStats:GetStats:Rejected", {
+          resolverId: aData.resolverId,
+          reason: "Invalid manifestURL"
+        });
+        return;
+      }
+    }
+
+    // Create an alarm object.
+    let newAlarm = {
+      type: aData.type,
+      component: aData.statsOptions.component || "",
+      serviceType: aData.statsOptions.serviceType || "",
+      manifestURL: manifestURL || "",
+      threshold: aData.threshold,
+      startTime: aData.startTime,
+      data: aData.data
+    };
+
+    // Execute DB operation.
+    let onAlarmAdded = this._createDbCallback(aMm, aData.resolverId,
+                                              "ResourceStats:AddAlarm");
+    this._db.addAlarm(newAlarm, onAlarmAdded);
+  },
+
+  getAlarms: function(aMm, aData) {
+    if (DEBUG) {
+      debug("getAlarms(): " + JSON.stringify(aData));
+    }
+
+    let options = null;
+    let statsOptions = aData.statsOptions;
+    // If all keys in statsOptions are set to null, treat this call as quering
+    // all alarms; otherwise, resolve the statsOptions and perform DB query
+    // according to the resolved result.
+    if (statsOptions.manifestURL || statsOptions.serviceType ||
+        statsOptions.component) {
+      // Validate manifestURL.
+      let manifestURL = statsOptions.manifestURL || "";
+      if (manifestURL) {
+        let appId = appsService.getAppLocalIdByManifestURL(manifestURL);
+
+        if (!appId) {
+          aMm.sendAsyncMessage("ResourceStats:GetStats:Rejected", {
+            resolverId: aData.resolverId,
+            reason: "Invalid manifestURL"
+          });
+          return;
+        }
+      }
+
+      options = {
+        manifestURL: manifestURL,
+        serviceType: statsOptions.serviceType || "",
+        component: statsOptions.component || ""
+      };
+    }
+
+    // Execute DB operation.
+    let onAlarmsGot = this._createDbCallback(aMm, aData.resolverId,
+                                            "ResourceStats:GetAlarms");
+    this._db.getAlarms(aData.type, options, onAlarmsGot);
+  },
+
+  removeAlarm: function(aMm, aData) {
+    if (DEBUG) {
+      debug("removeAlarm(): " + JSON.stringify(aData));
+    }
+
+    // Execute DB operation.
+    let onAlarmRemoved = function(aError, aResult) {
+      if (aError) {
+        aMm.sendAsyncMessage("ResourceStats:RemoveAlarm:Rejected",
+                             { resolverId: aData.resolverId, reason: aError });
+      }
+
+      if (!aResult) {
+        aMm.sendAsyncMessage("ResourceStats:RemoveAlarm:Rejected",
+                             { resolverId: aData.resolverId,
+                               reason: "alarm not existed" });
+      }
+
+      aMm.sendAsyncMessage("ResourceStats:RemoveAlarm:Resolved",
+                           { resolverId: aData.resolverId, value: aResult });
+    };
+
+    this._db.removeAlarm(aData.type, aData.alarmId, onAlarmRemoved);
+  },
+
+  removeAllAlarms: function(aMm, aData) {
+    if (DEBUG) {
+      debug("removeAllAlarms(): " + JSON.stringify(aData));
+    }
+
+    // Execute DB operation.
+    let onAllAlarmsRemoved = this._createDbCallback(aMm, aData.resolverId,
+                                                    "ResourceStats:RemoveAllAlarms");
+    this._db.removeAllAlarms(aData.type, onAllAlarmsRemoved);
+  },
+
+  getComponents: function(aMm, aData) {
+    if (DEBUG) {
+      debug("getComponents(): " + JSON.stringify(aData));
+    }
+
+    // Execute DB operation.
+    let onComponentsGot = this._createDbCallback(aMm, aData.resolverId,
+                                                 "ResourceStats:GetComponents");
+    this._db.getComponents(aData.type, onComponentsGot);
+  },
+};
+
+this.ResourceStatsService.init();
+
new file mode 100644
--- /dev/null
+++ b/dom/resourcestats/moz.build
@@ -0,0 +1,26 @@
+# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+EXPORTS.mozilla.dom += [
+    'ResourceStatsControl.h',
+]
+
+SOURCES += [
+    'ResourceStatsControl.cpp',
+]
+
+EXTRA_COMPONENTS += [
+    'ResourceStatsManager.js',
+    'ResourceStatsManager.manifest',
+]
+
+EXTRA_JS_MODULES += [
+    'ResourceStatsDB.jsm',
+    'ResourceStatsService.jsm',
+]
+
+FINAL_LIBRARY = 'gklayout'
+