Merge mozilla-central to mozilla-inbound
authorEd Morley <emorley@mozilla.com>
Mon, 07 Jan 2013 12:04:57 +0000
changeset 123491 2889e68bf6ca305dcc9887e438077aa346f7f39c
parent 123490 05895b39ed9eb088f05891e456139fba7822d545 (current diff)
parent 123439 605ae260b7c8afd3692ce97ce005a462eacf6095 (diff)
child 123492 c4966e14e726f492f394732b749cc2c38b20bf00
child 124032 924d2be32b1359494c00e84ed922af66bfb79360
push id3129
push userakeybl@mozilla.com
push dateMon, 07 Jan 2013 22:54:45 +0000
treeherdermozilla-aurora@090bc89ff6b4 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
milestone20.0a1
Merge mozilla-central to mozilla-inbound
services/metrics/tests/xpcshell/test_metrics_collection_result.js
services/metrics/tests/xpcshell/test_metrics_measurement.js
services/notifications/Makefile.in
services/notifications/NotificationsComponents.manifest
services/notifications/README
services/notifications/service.js
services/notifications/services-notifications.js
services/notifications/tests/Makefile.in
services/notifications/tests/unit/head_helpers.js
services/notifications/tests/unit/test_service_start.js
services/notifications/tests/unit/xpcshell.ini
--- a/b2g/installer/package-manifest.in
+++ b/b2g/installer/package-manifest.in
@@ -580,19 +580,16 @@
 @BINPATH@/defaults/autoconfig/platform.js
 @BINPATH@/defaults/autoconfig/prefcalls.js
 @BINPATH@/defaults/profile/prefs.js
 
 ; Services (gre) prefs
 #ifdef MOZ_SERVICES_SYNC
 @BINPATH@/defaults/pref/services-sync.js
 #endif
-#ifdef MOZ_SERVICES_HEALTHREPORT
-@BINPATH@/defaults/pref/healthreport-prefs.js
-#endif
 
 ; [Layout Engine Resources]
 ; Style Sheets, Graphics and other Resources used by the layout engine. 
 @BINPATH@/res/EditorOverride.css
 @BINPATH@/res/contenteditable.css
 @BINPATH@/res/designmode.css
 @BINPATH@/res/table-add-column-after-active.gif
 @BINPATH@/res/table-add-column-after-hover.gif
--- a/browser/confvars.sh
+++ b/browser/confvars.sh
@@ -24,17 +24,16 @@ if test "$OS_ARCH" = "WINNT"; then
 fi
 
 MOZ_CHROME_FILE_FORMAT=omni
 MOZ_SAFE_BROWSING=1
 MOZ_SERVICES_AITC=1
 MOZ_SERVICES_COMMON=1
 MOZ_SERVICES_CRYPTO=1
 MOZ_SERVICES_METRICS=1
-MOZ_SERVICES_NOTIFICATIONS=1
 MOZ_SERVICES_SYNC=1
 MOZ_APP_VERSION=$FIREFOX_VERSION
 MOZ_EXTENSIONS_DEFAULT=" gio"
 # MOZ_APP_DISPLAYNAME will be set by branding/configure.sh
 # Changing MOZ_*BRANDING_DIRECTORY requires a clobber to ensure correct results,
 # because branding dependencies are broken.
 # MOZ_BRANDING_DIRECTORY is the default branding directory used when none is
 # specified. It should never point to the "official" branding directory.
--- a/browser/devtools/debugger/test/browser_dbg_aaa_run_first_leaktest.js
+++ b/browser/devtools/debugger/test/browser_dbg_aaa_run_first_leaktest.js
@@ -26,38 +26,38 @@ function test()
 
   debug_tab_pane(TAB_URL, function(aTab, aDebuggee, aPane) {
     gTab = aTab;
     gDebuggee = aDebuggee;
     gPane = aPane;
     gDebugger = gPane.panelWin;
     resumed = true;
 
+    gDebugger.addEventListener("Debugger:SourceShown", onScriptShown);
+
     gDebugger.DebuggerController.activeThread.addOneTimeListener("framesadded", function() {
       framesAdded = true;
       executeSoon(startTest);
     });
 
     executeSoon(function() {
       gDebuggee.firstCall();
     });
   });
 
   function onScriptShown(aEvent)
   {
     scriptShown = aEvent.detail.url.indexOf("-02.js") != -1;
     executeSoon(startTest);
   }
 
-  window.addEventListener("Debugger:SourceShown", onScriptShown);
-
   function startTest()
   {
     if (scriptShown && framesAdded && resumed && !testStarted) {
-      window.removeEventListener("Debugger:SourceShown", onScriptShown);
+      gDebugger.removeEventListener("Debugger:SourceShown", onScriptShown);
       testStarted = true;
       Services.tm.currentThread.dispatch({ run: performTest }, 0);
     }
   }
 
   function performTest()
   {
     closeDebuggerAndFinish();
--- a/browser/devtools/debugger/test/browser_dbg_bug727429_watch-expressions-01.js
+++ b/browser/devtools/debugger/test/browser_dbg_bug727429_watch-expressions-01.js
@@ -111,17 +111,17 @@ function test()
 
     closeDebuggerAndFinish();
   }
 
   function addAndCheckCustomExpression(total, index, string, noBlur) {
     addAndCheckExpressions(total, index, "", true);
 
     for (let i = 0; i < string.length; i++) {
-      EventUtils.sendChar(string[i]);
+      EventUtils.sendChar(string[i], gDebugger);
     }
 
     gDebugger.editor.focus();
 
     let id = gWatch.getItemAtIndex(index).attachment.id;
     let element = gDebugger.document.getElementById("expression-" + id);
 
     is(gWatch.getItemAtIndex(index).attachment.initialExpression, "",
--- a/browser/devtools/debugger/test/browser_dbg_bug727429_watch-expressions-02.js
+++ b/browser/devtools/debugger/test/browser_dbg_bug727429_watch-expressions-02.js
@@ -121,65 +121,65 @@ function test()
   function test4(callback) {
     waitForWatchExpressions(function() {
       info("Performing test4");
       checkWatchExpressions(5, "sensational", 13);
       callback();
     });
     executeSoon(function() {
       gWatch.addExpression("a = 5");
-      EventUtils.sendKey("RETURN");
+      EventUtils.sendKey("RETURN", gDebugger);
     });
   }
 
   function test5(callback) {
     waitForWatchExpressions(function() {
       info("Performing test5");
       checkWatchExpressions(5, "sensational", 13);
       callback();
     });
     executeSoon(function() {
       gWatch.addExpression("encodeURI(\"\\\")");
-      EventUtils.sendKey("RETURN");
+      EventUtils.sendKey("RETURN", gDebugger);
     });
   }
 
   function test6(callback) {
     waitForWatchExpressions(function() {
       info("Performing test6");
       checkWatchExpressions(5, "sensational", 13);
       callback();
     })
     executeSoon(function() {
       gWatch.addExpression("decodeURI(\"\\\")");
-      EventUtils.sendKey("RETURN");
+      EventUtils.sendKey("RETURN", gDebugger);
     });
   }
 
   function test7(callback) {
     waitForWatchExpressions(function() {
       info("Performing test7");
       checkWatchExpressions(5, "sensational", 13);
       callback();
     });
     executeSoon(function() {
       gWatch.addExpression("?");
-      EventUtils.sendKey("RETURN");
+      EventUtils.sendKey("RETURN", gDebugger);
     });
   }
 
   function test8(callback) {
     waitForWatchExpressions(function() {
       info("Performing test8");
       checkWatchExpressions(5, "sensational", 13);
       callback();
     });
     executeSoon(function() {
       gWatch.addExpression("a");
-      EventUtils.sendKey("RETURN");
+      EventUtils.sendKey("RETURN", gDebugger);
     });
   }
 
   function test9(callback) {
     waitForAfterFramesCleared(function() {
       info("Performing test9");
       callback();
     });
--- a/browser/devtools/debugger/test/browser_dbg_bug731394_editor-contextmenu.js
+++ b/browser/devtools/debugger/test/browser_dbg_bug731394_editor-contextmenu.js
@@ -26,38 +26,38 @@ function test()
 
   debug_tab_pane(TAB_URL, function(aTab, aDebuggee, aPane) {
     gTab = aTab;
     gDebuggee = aDebuggee;
     gPane = aPane;
     gDebugger = gPane.panelWin;
     resumed = true;
 
+    gDebugger.addEventListener("Debugger:SourceShown", onScriptShown);
+
     gDebugger.DebuggerController.activeThread.addOneTimeListener("framesadded", function() {
       framesAdded = true;
       executeSoon(startTest);
     });
 
     executeSoon(function() {
       gDebuggee.firstCall();
     });
   });
 
   function onScriptShown(aEvent) {
     scriptShown = aEvent.detail.url.indexOf("-02.js") != -1;
     executeSoon(startTest);
   }
 
-  window.addEventListener("Debugger:SourceShown", onScriptShown);
-
   function startTest()
   {
     if (scriptShown && framesAdded && resumed && !testStarted) {
       testStarted = true;
-      window.removeEventListener("Debugger:SourceShown", onScriptShown);
+      gDebugger.removeEventListener("Debugger:SourceShown", onScriptShown);
       Services.tm.currentThread.dispatch({ run: performTest }, 0);
     }
   }
 
   function performTest()
   {
     let scripts = gDebugger.DebuggerView.Sources._container;
 
--- a/browser/devtools/debugger/test/browser_dbg_bug740825_conditional-breakpoints-01.js
+++ b/browser/devtools/debugger/test/browser_dbg_bug740825_conditional-breakpoints-01.js
@@ -31,38 +31,38 @@ function test()
     gPane = aPane;
     gDebugger = gPane.panelWin;
     gBreakpoints = gDebugger.DebuggerController.Breakpoints;
     gBreakpointsPane = gDebugger.DebuggerView.Breakpoints;
 
     gDebugger.DebuggerView.togglePanes({ visible: true, animated: false });
     resumed = true;
 
+    gDebugger.addEventListener("Debugger:SourceShown", onScriptShown);
+
     gDebugger.DebuggerController.activeThread.addOneTimeListener("framesadded", function() {
       framesAdded = true;
       executeSoon(startTest);
     });
 
     executeSoon(function() {
       gDebuggee.ermahgerd(); // ermahgerd!!
     });
   });
 
   function onScriptShown(aEvent)
   {
     scriptShown = aEvent.detail.url.indexOf("conditional-breakpoints") != -1;
     executeSoon(startTest);
   }
 
-  window.addEventListener("Debugger:SourceShown", onScriptShown);
-
   function startTest()
   {
     if (scriptShown && framesAdded && resumed && !testStarted) {
-      window.removeEventListener("Debugger:SourceShown", onScriptShown);
+      gDebugger.removeEventListener("Debugger:SourceShown", onScriptShown);
       testStarted = true;
       Services.tm.currentThread.dispatch({ run: addBreakpoints }, 0);
     }
   }
 
   function performTest()
   {
     gScripts = gDebugger.DebuggerView.Sources;
--- a/browser/devtools/debugger/test/browser_dbg_bug740825_conditional-breakpoints-02.js
+++ b/browser/devtools/debugger/test/browser_dbg_bug740825_conditional-breakpoints-02.js
@@ -32,16 +32,18 @@ function test()
   debug_tab_pane(TAB_URL, function(aTab, aDebuggee, aPane) {
     gTab = aTab;
     gDebuggee = aDebuggee;
     gPane = aPane;
     gDebugger = gPane.panelWin;
     gBreakpoints = gDebugger.DebuggerController.Breakpoints;
     gBreakpointsPane = gDebugger.DebuggerView.Breakpoints;
 
+    gDebugger.addEventListener("Debugger:SourceShown", onScriptShown);
+
     gDebugger.DebuggerView.togglePanes({ visible: true, animated: false });
     resumed = true;
 
     gDebugger.DebuggerController.activeThread.addOneTimeListener("framesadded", function() {
       framesAdded = true;
       executeSoon(startTest);
     });
 
@@ -51,22 +53,20 @@ function test()
   });
 
   function onScriptShown(aEvent)
   {
     scriptShown = aEvent.detail.url.indexOf("conditional-breakpoints") != -1;
     executeSoon(startTest);
   }
 
-  window.addEventListener("Debugger:SourceShown", onScriptShown);
-
   function startTest()
   {
     if (scriptShown && framesAdded && resumed && !testStarted) {
-      window.removeEventListener("Debugger:SourceShown", onScriptShown);
+      gDebugger.removeEventListener("Debugger:SourceShown", onScriptShown);
       testStarted = true;
       Services.tm.currentThread.dispatch({ run: performTest }, 0);
     }
   }
 
   function performTest()
   {
     gScripts = gDebugger.DebuggerView.Sources;
@@ -161,17 +161,17 @@ function test()
         });
       });
     });
   }
 
   function modBreakpoint3()
   {
     write("bamboocha");
-    EventUtils.sendKey("RETURN");
+    EventUtils.sendKey("RETURN", gDebugger);
 
     waitForBreakpoint(14, function() {
       waitForCaretPos(13, function() {
         waitForPopup(false, function() {
           is(gBreakpointsPane.selectedClient.conditionalExpression, "bamboocha",
             "The bamboocha expression wasn't fonud on the conditional breakpoint");
 
           executeSoon(setContextMenu);
@@ -563,17 +563,17 @@ function test()
     clear();
     append(text);
   }
 
   function append(text) {
     gBreakpointsPane._cbTextbox.focus();
 
     for (let i = 0; i < text.length; i++) {
-      EventUtils.sendChar(text[i]);
+      EventUtils.sendChar(text[i], gDebugger);
     }
   }
 
   registerCleanupFunction(function() {
     removeTab(gTab);
     gPane = null;
     gTab = null;
     gDebuggee = null;
--- a/browser/devtools/debugger/test/browser_dbg_location-changes-blank.js
+++ b/browser/devtools/debugger/test/browser_dbg_location-changes-blank.js
@@ -17,33 +17,33 @@ function test()
   let framesAdded = false;
 
   debug_tab_pane(STACK_URL, function(aTab, aDebuggee, aPane) {
     gTab = aTab;
     gDebuggee = aDebuggee;
     gPane = aPane;
     gDebugger = gPane.panelWin;
 
+    gDebugger.addEventListener("Debugger:SourceShown", function _onEvent(aEvent) {
+      let url = aEvent.detail.url;
+      if (url.indexOf("browser_dbg_stack") != -1) {
+        scriptShown = true;
+        gDebugger.removeEventListener(aEvent.type, _onEvent);
+        runTest();
+      }
+    });
+
     gDebugger.DebuggerController.activeThread.addOneTimeListener("framesadded", function() {
       framesAdded = true;
       runTest();
     });
 
     gDebuggee.simpleCall();
   });
 
-  window.addEventListener("Debugger:SourceShown", function _onEvent(aEvent) {
-    let url = aEvent.detail.url;
-    if (url.indexOf("browser_dbg_stack") != -1) {
-      scriptShown = true;
-      window.removeEventListener(aEvent.type, _onEvent);
-      runTest();
-    }
-  });
-
   function runTest()
   {
     if (scriptShown && framesAdded) {
       Services.tm.currentThread.dispatch({ run: testSimpleCall }, 0);
     }
   }
 }
 
--- a/browser/devtools/debugger/test/browser_dbg_location-changes-new.js
+++ b/browser/devtools/debugger/test/browser_dbg_location-changes-new.js
@@ -17,33 +17,33 @@ function test()
   let framesAdded = false;
 
   debug_tab_pane(STACK_URL, function(aTab, aDebuggee, aPane) {
     gTab = aTab;
     gDebuggee = aDebuggee;
     gPane = aPane;
     gDebugger = gPane.panelWin;
 
+    gDebugger.addEventListener("Debugger:SourceShown", function _onEvent(aEvent) {
+      let url = aEvent.detail.url;
+      if (url.indexOf("browser_dbg_stack") != -1) {
+        scriptShown = true;
+        gDebugger.removeEventListener(aEvent.type, _onEvent);
+        runTest();
+      }
+    });
+
     gDebugger.DebuggerController.activeThread.addOneTimeListener("framesadded", function() {
       framesAdded = true;
       runTest();
     });
 
     gDebuggee.simpleCall();
   });
 
-  window.addEventListener("Debugger:SourceShown", function _onEvent(aEvent) {
-    let url = aEvent.detail.url;
-    if (url.indexOf("browser_dbg_stack") != -1) {
-      scriptShown = true;
-      window.removeEventListener(aEvent.type, _onEvent);
-      runTest();
-    }
-  });
-
   function runTest()
   {
     if (scriptShown && framesAdded) {
       Services.tm.currentThread.dispatch({ run: testSimpleCall }, 0);
     }
   }
 }
 
--- a/browser/devtools/debugger/test/browser_dbg_propertyview-edit-watch.js
+++ b/browser/devtools/debugger/test/browser_dbg_propertyview-edit-watch.js
@@ -149,18 +149,18 @@ function testFrameEval() {
     gDebuggee.ermahgerd(); // ermahgerd!!
   });
 }
 
 function testModification(aVar, aTest, aCallback, aNewValue, aNewResult, aArgResult,
                           aLocalScopeIndex = 1, aDeletionFlag = null)
 {
   function makeChangesAndExitInputMode() {
-    EventUtils.sendString(aNewValue);
-    EventUtils.sendKey("RETURN");
+    EventUtils.sendString(aNewValue, gDebugger);
+    EventUtils.sendKey("RETURN", gDebugger);
   }
 
   EventUtils.sendMouseEvent({ type: "dblclick" },
     aVar.querySelector(".name"),
     gDebugger);
 
   executeSoon(function() {
     ok(aVar.querySelector(".element-name-input"),
--- a/browser/devtools/debugger/test/browser_dbg_propertyview-edit.js
+++ b/browser/devtools/debugger/test/browser_dbg_propertyview-edit.js
@@ -60,18 +60,18 @@ function testFrameEval() {
 
   EventUtils.sendMouseEvent({ type: "click" },
     content.document.querySelector("button"),
     content.window);
 }
 
 function testModification(aVar, aCallback, aNewValue, aNewResult) {
   function makeChangesAndExitInputMode() {
-    EventUtils.sendString(aNewValue);
-    EventUtils.sendKey("RETURN");
+    EventUtils.sendString(aNewValue, gDebugger);
+    EventUtils.sendKey("RETURN", gDebugger);
   }
 
   EventUtils.sendMouseEvent({ type: "click" },
     aVar.querySelector(".value"),
     gDebugger);
 
   executeSoon(function() {
     ok(aVar.querySelector(".element-value-input"),
--- a/browser/devtools/debugger/test/browser_dbg_propertyview-filter-01.js
+++ b/browser/devtools/debugger/test/browser_dbg_propertyview-filter-01.js
@@ -497,17 +497,17 @@ function write(text) {
   clear();
   append(text);
 }
 
 function append(text) {
   gSearchBox.focus();
 
   for (let i = 0; i < text.length; i++) {
-    EventUtils.sendChar(text[i]);
+    EventUtils.sendChar(text[i], gDebugger);
   }
 }
 
 registerCleanupFunction(function() {
   removeTab(gTab);
   gPane = null;
   gTab = null;
   gDebugger = null;
--- a/browser/devtools/debugger/test/browser_dbg_propertyview-filter-02.js
+++ b/browser/devtools/debugger/test/browser_dbg_propertyview-filter-02.js
@@ -439,17 +439,17 @@ function write(text) {
   clear();
   append(text);
 }
 
 function append(text) {
   gSearchBox.focus();
 
   for (let i = 0; i < text.length; i++) {
-    EventUtils.sendChar(text[i]);
+    EventUtils.sendChar(text[i], gDebugger);
   }
 }
 
 registerCleanupFunction(function() {
   removeTab(gTab);
   gPane = null;
   gTab = null;
   gDebugger = null;
--- a/browser/devtools/debugger/test/browser_dbg_propertyview-filter-05.js
+++ b/browser/devtools/debugger/test/browser_dbg_propertyview-filter-05.js
@@ -64,17 +64,17 @@ function testVariablesFiltering()
 
     is(loadScope.querySelectorAll(".variable:not([non-match]) > .title > .name")[0].getAttribute("value"),
       "button", "The only load variable displayed should be 'button'");
 
     let oneItem = innerScopeItem.get("one");
     is(oneItem.expanded, false,
       "The one item in the inner scope should not be expanded");
 
-    EventUtils.sendKey("RETURN");
+    EventUtils.sendKey("RETURN", gDebugger);
     is(oneItem.expanded, true,
       "The one item in the inner scope should now be expanded");
   }
 
   function test2()
   {
     write("*two");
     ignoreExtraMatchedProperties();
@@ -103,17 +103,17 @@ function testVariablesFiltering()
 
     is(innerScope.querySelectorAll(".variable:not([non-match]) > .title > .name")[0].getAttribute("value"),
       "two", "The only inner variable displayed should be 'two'");
 
     let twoItem = innerScopeItem.get("two");
     is(twoItem.expanded, false,
       "The two item in the inner scope should not be expanded");
 
-    EventUtils.sendKey("RETURN");
+    EventUtils.sendKey("RETURN", gDebugger);
     is(twoItem.expanded, true,
       "The two item in the inner scope should now be expanded");
   }
 
   function test3()
   {
     backspace(3);
     ignoreExtraMatchedProperties();
@@ -280,25 +280,25 @@ function clear() {
 
 function write(text) {
   clear();
   append(text);
 }
 
 function backspace(times) {
   for (let i = 0; i < times; i++) {
-    EventUtils.sendKey("BACK_SPACE")
+    EventUtils.sendKey("BACK_SPACE", gDebugger)
   }
 }
 
 function append(text) {
   gSearchBox.focus();
 
   for (let i = 0; i < text.length; i++) {
-    EventUtils.sendChar(text[i]);
+    EventUtils.sendChar(text[i], gDebugger);
   }
 }
 
 registerCleanupFunction(function() {
   removeTab(gTab);
   gPane = null;
   gTab = null;
   gDebugger = null;
--- a/browser/devtools/debugger/test/browser_dbg_propertyview-filter-06.js
+++ b/browser/devtools/debugger/test/browser_dbg_propertyview-filter-06.js
@@ -36,43 +36,43 @@ function testVariablesFiltering()
     test1: function()
     {
       assertExpansion(1, [true, false, false, false, false]);
       clear();
     },
     test2: function()
     {
       assertExpansion(2, [true, false, false, false, false]);
-      EventUtils.sendKey("RETURN");
+      EventUtils.sendKey("RETURN", gDebugger);
     },
     test3: function()
     {
       assertExpansion(3, [true, false, false, false, false]);
       gDebugger.editor.focus();
     },
     test4: function()
     {
       assertExpansion(4, [true, false, false, false, false]);
       write("*");
     },
     test5: function() {
       assertExpansion(5, [true, true, true, true, true]);
-      EventUtils.sendKey("RETURN");
+      EventUtils.sendKey("RETURN", gDebugger);
     },
     test6: function() {
       assertExpansion(6, [true, true, true, true, true]);
       gDebugger.editor.focus();
     },
     test7: function() {
       assertExpansion(7, [true, true, true, true, true]);
       backspace(1);
     },
     test8: function() {
       assertExpansion(8, [true, true, true, true, true]);
-      EventUtils.sendKey("RETURN");
+      EventUtils.sendKey("RETURN", gDebugger);
     },
     test9: function() {
       assertExpansion(9, [true, true, true, true, true]);
       gDebugger.editor.focus();
     },
     test10: function() {
       assertExpansion(10, [true, true, true, true, true]);
       innerScopeItem.collapse();
@@ -82,41 +82,41 @@ function testVariablesFiltering()
       globalScopeItem.collapse();
     },
     test11: function() {
       assertExpansion(11, [false, false, false, false, false]);
       clear();
     },
     test12: function() {
       assertExpansion(12, [false, false, false, false, false]);
-      EventUtils.sendKey("RETURN");
+      EventUtils.sendKey("RETURN", gDebugger);
     },
     test13: function() {
       assertExpansion(13, [false, false, false, false, false]);
       gDebugger.editor.focus();
     },
     test14: function() {
       assertExpansion(14, [false, false, false, false, false]);
       write("*");
     },
     test15: function() {
       assertExpansion(15, [true, true, true, true, true]);
-      EventUtils.sendKey("RETURN");
+      EventUtils.sendKey("RETURN", gDebugger);
     },
     test16: function() {
       assertExpansion(16, [true, true, true, true, true]);
       gDebugger.editor.focus();
     },
     test17: function() {
       assertExpansion(17, [true, true, true, true, true]);
       backspace(1);
     },
     test18: function() {
       assertExpansion(18, [true, true, true, true, true]);
-      EventUtils.sendKey("RETURN");
+      EventUtils.sendKey("RETURN", gDebugger);
     },
     test19: function() {
       assertExpansion(19, [true, true, true, true, true]);
       gDebugger.editor.focus();
     },
     test20: function() {
       assertExpansion(20, [true, true, true, true, true]);
     }
@@ -222,25 +222,25 @@ function clear() {
 
 function write(text) {
   clear();
   append(text);
 }
 
 function backspace(times) {
   for (let i = 0; i < times; i++) {
-    EventUtils.sendKey("BACK_SPACE")
+    EventUtils.sendKey("BACK_SPACE", gDebugger)
   }
 }
 
 function append(text) {
   gSearchBox.focus();
 
   for (let i = 0; i < text.length; i++) {
-    EventUtils.sendChar(text[i]);
+    EventUtils.sendChar(text[i], gDebugger);
   }
 }
 
 registerCleanupFunction(function() {
   removeTab(gTab);
   gPane = null;
   gTab = null;
   gDebugger = null;
--- a/browser/devtools/debugger/test/browser_dbg_propertyview-filter-07.js
+++ b/browser/devtools/debugger/test/browser_dbg_propertyview-filter-07.js
@@ -36,43 +36,43 @@ function testVariablesFiltering()
     test1: function()
     {
       assertExpansion(1, [true, false, false, false, false]);
       clear();
     },
     test2: function()
     {
       assertExpansion(2, [true, false, false, false, false]);
-      EventUtils.sendKey("RETURN");
+      EventUtils.sendKey("RETURN", gDebugger);
     },
     test3: function()
     {
       assertExpansion(3, [true, false, false, false, false]);
       gDebugger.editor.focus();
     },
     test4: function()
     {
       assertExpansion(4, [true, false, false, false, false]);
       write("*");
     },
     test5: function() {
       assertExpansion(5, [true, true, true, true, true]);
-      EventUtils.sendKey("RETURN");
+      EventUtils.sendKey("RETURN", gDebugger);
     },
     test6: function() {
       assertExpansion(6, [true, true, true, true, true]);
       gDebugger.editor.focus();
     },
     test7: function() {
       assertExpansion(7, [true, true, true, true, true]);
       backspace(1);
     },
     test8: function() {
       assertExpansion(8, [true, true, true, true, true]);
-      EventUtils.sendKey("RETURN");
+      EventUtils.sendKey("RETURN", gDebugger);
     },
     test9: function() {
       assertExpansion(9, [true, true, true, true, true]);
       gDebugger.editor.focus();
     },
     test10: function() {
       assertExpansion(10, [true, true, true, true, true]);
       innerScopeItem.collapse();
@@ -82,41 +82,41 @@ function testVariablesFiltering()
       globalScopeItem.collapse();
     },
     test11: function() {
       assertExpansion(11, [false, false, false, false, false]);
       clear();
     },
     test12: function() {
       assertExpansion(12, [false, false, false, false, false]);
-      EventUtils.sendKey("RETURN");
+      EventUtils.sendKey("RETURN", gDebugger);
     },
     test13: function() {
       assertExpansion(13, [false, false, false, false, false]);
       gDebugger.editor.focus();
     },
     test14: function() {
       assertExpansion(14, [false, false, false, false, false]);
       write("*");
     },
     test15: function() {
       assertExpansion(15, [true, true, true, true, true]);
-      EventUtils.sendKey("RETURN");
+      EventUtils.sendKey("RETURN", gDebugger);
     },
     test16: function() {
       assertExpansion(16, [true, true, true, true, true]);
       gDebugger.editor.focus();
     },
     test17: function() {
       assertExpansion(17, [true, true, true, true, true]);
       backspace(1);
     },
     test18: function() {
       assertExpansion(18, [true, true, true, true, true]);
-      EventUtils.sendKey("RETURN");
+      EventUtils.sendKey("RETURN", gDebugger);
     },
     test19: function() {
       assertExpansion(19, [true, true, true, true, true]);
       gDebugger.editor.focus();
     },
     test20: function() {
       assertExpansion(20, [true, true, true, true, true]);
     }
@@ -227,25 +227,25 @@ function clear() {
 
 function write(text) {
   clear();
   append(text);
 }
 
 function backspace(times) {
   for (let i = 0; i < times; i++) {
-    EventUtils.sendKey("BACK_SPACE")
+    EventUtils.sendKey("BACK_SPACE", gDebugger);
   }
 }
 
 function append(text) {
   gSearchBox.focus();
 
   for (let i = 0; i < text.length; i++) {
-    EventUtils.sendChar(text[i]);
+    EventUtils.sendChar(text[i], gDebugger);
   }
 }
 
 registerCleanupFunction(function() {
   removeTab(gTab);
   gPane = null;
   gTab = null;
   gDebugger = null;
--- a/browser/devtools/debugger/test/browser_dbg_propertyview-filter-08.js
+++ b/browser/devtools/debugger/test/browser_dbg_propertyview-filter-08.js
@@ -42,17 +42,17 @@ function testVariablesFiltering()
     test2: function(aCallback)
     {
       is(testScopeItem.get("arguments").expanded, false,
         "The arguments pseudoarray in the testScope should not be expanded");
       is(loadScopeItem.get("arguments").expanded, false,
         "The arguments pseudoarray in the testScope should not be expanded");
 
       assertExpansion(1, [true, true, true, true, true]);
-      EventUtils.sendKey("RETURN");
+      EventUtils.sendKey("RETURN", gDebugger);
       aCallback();
     },
     test3: function(aCallback)
     {
       is(testScopeItem.get("arguments").expanded, true,
         "The arguments pseudoarray in the testScope should now be expanded");
       is(loadScopeItem.get("arguments").expanded, true,
         "The arguments pseudoarray in the testScope should now be expanded");
@@ -297,25 +297,25 @@ function clear() {
 
 function write(text) {
   clear();
   append(text);
 }
 
 function backspace(times) {
   for (let i = 0; i < times; i++) {
-    EventUtils.sendKey("BACK_SPACE")
+    EventUtils.sendKey("BACK_SPACE", gDebugger);
   }
 }
 
 function append(text) {
   gSearchBox.focus();
 
   for (let i = 0; i < text.length; i++) {
-    EventUtils.sendChar(text[i]);
+    EventUtils.sendChar(text[i], gDebugger);
   }
 }
 
 registerCleanupFunction(function() {
   removeTab(gTab);
   gPane = null;
   gTab = null;
   gDebugger = null;
--- a/browser/devtools/debugger/test/browser_dbg_reload-preferred-script.js
+++ b/browser/devtools/debugger/test/browser_dbg_reload-preferred-script.js
@@ -26,33 +26,33 @@ function test()
   debug_tab_pane(TAB_URL, function(aTab, aDebuggee, aPane) {
     gTab = aTab;
     gDebuggee = aDebuggee;
     gPane = aPane;
     gDebugger = gPane.panelWin;
     gView = gDebugger.DebuggerView;
     resumed = true;
 
+    gDebugger.addEventListener("Debugger:SourceShown", onScriptShown);
+
     gView.Sources.preferredSource = EXAMPLE_URL + expectedScript;
     startTest();
   });
 
   function onScriptShown(aEvent)
   {
     expectedScriptShown = aEvent.detail.url.indexOf(expectedScript) != -1;
     scriptShownUrl = aEvent.detail.url;
     startTest();
   }
 
-  window.addEventListener("Debugger:SourceShown", onScriptShown);
-
   function startTest()
   {
     if (expectedScriptShown && resumed && !testStarted) {
-      window.removeEventListener("Debugger:SourceShown", onScriptShown);
+      gDebugger.removeEventListener("Debugger:SourceShown", onScriptShown);
       testStarted = true;
       Services.tm.currentThread.dispatch({ run: performTest }, 0);
     }
   }
 
   function performTest()
   {
     info("Currently preferred script: " + gView.Sources.preferredValue);
--- a/browser/devtools/debugger/test/browser_dbg_reload-same-script.js
+++ b/browser/devtools/debugger/test/browser_dbg_reload-same-script.js
@@ -27,16 +27,18 @@ function test()
   debug_tab_pane(TAB_URL, function(aTab, aDebuggee, aPane) {
     gTab = aTab;
     gDebuggee = aDebuggee;
     gPane = aPane;
     gDebugger = gPane.panelWin;
     gView = gDebugger.DebuggerView;
     resumed = true;
 
+    gDebugger.addEventListener("Debugger:SourceShown", onScriptShown);
+
     startTest();
   });
 
   function onScriptShown(aEvent)
   {
     expectedScriptShown = aEvent.detail.url.indexOf("-01.js") != -1;
     scriptShownUrl = aEvent.detail.url;
     startTest();
@@ -50,32 +52,30 @@ function test()
     info("The expected script for this ScriptShown event is: " + expectedScript);
     info("The current script for this ScriptShown event is: " + aEvent.detail.url);
 
     expectedScriptShown = aEvent.detail.url.indexOf(expectedScript) != -1;
     scriptShownUrl = aEvent.detail.url;
     testScriptShown();
   }
 
-  window.addEventListener("Debugger:SourceShown", onScriptShown);
-
   function startTest()
   {
     if (expectedScriptShown && resumed && !testStarted) {
-      window.removeEventListener("Debugger:SourceShown", onScriptShown);
-      window.addEventListener("Debugger:SourceShown", onUlteriorScriptShown);
+      gDebugger.removeEventListener("Debugger:SourceShown", onScriptShown);
+      gDebugger.addEventListener("Debugger:SourceShown", onUlteriorScriptShown);
       testStarted = true;
       Services.tm.currentThread.dispatch({ run: performTest }, 0);
     }
   }
 
   function finishTest()
   {
     if (expectedScriptShown && resumed && testStarted) {
-      window.removeEventListener("Debugger:SourceShown", onUlteriorScriptShown);
+      gDebugger.removeEventListener("Debugger:SourceShown", onUlteriorScriptShown);
       closeDebuggerAndFinish();
     }
   }
 
   function performTest()
   {
     testCurrentScript("-01.js", step);
     expectedScript = "-01.js";
--- a/browser/devtools/debugger/test/browser_dbg_script-switching.js
+++ b/browser/devtools/debugger/test/browser_dbg_script-switching.js
@@ -23,38 +23,38 @@ function test()
 
   debug_tab_pane(TAB_URL, function(aTab, aDebuggee, aPane) {
     gTab = aTab;
     gDebuggee = aDebuggee;
     gPane = aPane;
     gDebugger = gPane.panelWin;
     resumed = true;
 
+    gDebugger.addEventListener("Debugger:SourceShown", onScriptShown);
+
     gDebugger.DebuggerController.activeThread.addOneTimeListener("framesadded", function() {
       framesAdded = true;
       executeSoon(startTest);
     });
 
     executeSoon(function() {
       gDebuggee.firstCall();
     });
   });
 
   function onScriptShown(aEvent)
   {
     scriptShown = aEvent.detail.url.indexOf("-02.js") != -1;
     executeSoon(startTest);
   }
 
-  window.addEventListener("Debugger:SourceShown", onScriptShown);
-
   function startTest()
   {
     if (scriptShown && framesAdded && resumed && !testStarted) {
-      window.removeEventListener("Debugger:SourceShown", onScriptShown);
+      gDebugger.removeEventListener("Debugger:SourceShown", onScriptShown);
       testStarted = true;
       Services.tm.currentThread.dispatch({ run: testScriptsDisplay }, 0);
     }
   }
 }
 
 function testScriptsDisplay() {
   gScripts = gDebugger.DebuggerView.Sources._container;
@@ -82,20 +82,20 @@ function testScriptsDisplay() {
     label2), "Second script label is incorrect.");
 
   ok(gDebugger.editor.getText().search(/debugger/) != -1,
     "The correct script was loaded initially.");
 
   is(gDebugger.editor.getDebugLocation(), 5,
      "editor debugger location is correct.");
 
-  window.addEventListener("Debugger:SourceShown", function _onEvent(aEvent) {
+  gDebugger.addEventListener("Debugger:SourceShown", function _onEvent(aEvent) {
     let url = aEvent.detail.url;
     if (url.indexOf("-01.js") != -1) {
-      window.removeEventListener(aEvent.type, _onEvent);
+      gDebugger.removeEventListener(aEvent.type, _onEvent);
       testSwitchPaused();
     }
   });
 
   gDebugger.DebuggerView.Sources.selectedValue = EXAMPLE_URL + label1;
 }
 
 function testSwitchPaused()
@@ -105,20 +105,20 @@ function testSwitchPaused()
 
   ok(gDebugger.editor.getText().search(/firstCall/) != -1,
     "The first script is displayed.");
 
   is(gDebugger.editor.getDebugLocation(), -1,
      "editor debugger location has been cleared.");
 
   gDebugger.DebuggerController.activeThread.resume(function() {
-    window.addEventListener("Debugger:SourceShown", function _onEvent(aEvent) {
+    gDebugger.addEventListener("Debugger:SourceShown", function _onEvent(aEvent) {
       let url = aEvent.detail.url;
       if (url.indexOf("-02.js") != -1) {
-        window.removeEventListener(aEvent.type, _onEvent);
+        gDebugger.removeEventListener(aEvent.type, _onEvent);
         testSwitchRunning();
       }
     });
 
     gDebugger.DebuggerView.Sources.selectedValue = EXAMPLE_URL +
                                                    "test-script-switching-02.js";
   });
 }
--- a/browser/devtools/debugger/test/browser_dbg_scripts-searching-01.js
+++ b/browser/devtools/debugger/test/browser_dbg_scripts-searching-01.js
@@ -21,30 +21,30 @@ function test()
   let framesAdded = false;
 
   debug_tab_pane(STACK_URL, function(aTab, aDebuggee, aPane) {
     gTab = aTab;
     gDebuggee = aDebuggee;
     gPane = aPane;
     gDebugger = gPane.panelWin;
 
+    gDebugger.addEventListener("Debugger:SourceShown", function _onEvent(aEvent) {
+      gDebugger.removeEventListener(aEvent.type, _onEvent);
+      scriptShown = true;
+      runTest();
+    });
+
     gDebugger.DebuggerController.activeThread.addOneTimeListener("framesadded", function() {
       framesAdded = true;
       runTest();
     });
 
     gDebuggee.simpleCall();
   });
 
-  window.addEventListener("Debugger:SourceShown", function _onEvent(aEvent) {
-    window.removeEventListener(aEvent.type, _onEvent);
-    scriptShown = true;
-    runTest();
-  });
-
   function runTest()
   {
     if (scriptShown && framesAdded) {
       Services.tm.currentThread.dispatch({ run: testScriptSearching }, 0);
     }
   }
 }
 
@@ -58,78 +58,78 @@ function testScriptSearching() {
     gSearchBox = gDebugger.DebuggerView.Filtering._searchbox;
     gMenulist = gScripts._container;
 
     write(":12");
     ok(gEditor.getCaretPosition().line == 11 &&
        gEditor.getCaretPosition().col == 0,
       "The editor didn't jump to the correct line.");
 
-    EventUtils.synthesizeKey("g", { metaKey: true });
+    EventUtils.synthesizeKey("g", { metaKey: true }, gDebugger);
     ok(gEditor.getCaretPosition().line == 12 &&
        gEditor.getCaretPosition().col == 0,
       "The editor didn't jump to the correct line after Meta+G");
 
-    EventUtils.synthesizeKey("n", { ctrlKey: true });
+    EventUtils.synthesizeKey("n", { ctrlKey: true }, gDebugger);
     ok(gEditor.getCaretPosition().line == 13 &&
        gEditor.getCaretPosition().col == 0,
       "The editor didn't jump to the correct line after Ctrl+N");
 
-    EventUtils.synthesizeKey("G", { metaKey: true, shiftKey: true });
+    EventUtils.synthesizeKey("G", { metaKey: true, shiftKey: true }, gDebugger);
     ok(gEditor.getCaretPosition().line == 12 &&
        gEditor.getCaretPosition().col == 0,
       "The editor didn't jump to the correct line after Meta+Shift+G");
 
-    EventUtils.synthesizeKey("p", { ctrlKey: true });
+    EventUtils.synthesizeKey("p", { ctrlKey: true }, gDebugger);
     ok(gEditor.getCaretPosition().line == 11 &&
        gEditor.getCaretPosition().col == 0,
       "The editor didn't jump to the correct line after Ctrl+P");
 
     for (let i = 0; i < 100; i++) {
-      EventUtils.sendKey("DOWN");
+      EventUtils.sendKey("DOWN", gDebugger);
     }
     ok(gEditor.getCaretPosition().line == 32 &&
        gEditor.getCaretPosition().col == 0,
       "The editor didn't jump to the correct line after multiple DOWN keys");
 
     for (let i = 0; i < 100; i++) {
-      EventUtils.sendKey("UP");
+      EventUtils.sendKey("UP", gDebugger);
     }
     ok(gEditor.getCaretPosition().line == 0 &&
        gEditor.getCaretPosition().col == 0,
       "The editor didn't jump to the correct line after multiple UP keys");
 
 
     token = "debugger";
     write("#" + token);
     ok(gEditor.getCaretPosition().line == 2 &&
        gEditor.getCaretPosition().col == 44 + token.length,
       "The editor didn't jump to the correct token. (1)");
 
-    EventUtils.sendKey("DOWN");
+    EventUtils.sendKey("DOWN", gDebugger);
     ok(gEditor.getCaretPosition().line == 8 &&
        gEditor.getCaretPosition().col == 2 + token.length,
       "The editor didn't jump to the correct token. (2)");
 
-    EventUtils.sendKey("DOWN");
+    EventUtils.sendKey("DOWN", gDebugger);
     ok(gEditor.getCaretPosition().line == 12 &&
        gEditor.getCaretPosition().col == 8 + token.length,
       "The editor didn't jump to the correct token. (3)");
 
-    EventUtils.sendKey("RETURN");
+    EventUtils.sendKey("RETURN", gDebugger);
     ok(gEditor.getCaretPosition().line == 19 &&
        gEditor.getCaretPosition().col == 4 + token.length,
       "The editor didn't jump to the correct token. (4)");
 
-    EventUtils.sendKey("ENTER");
+    EventUtils.sendKey("ENTER", gDebugger);
     ok(gEditor.getCaretPosition().line == 2 &&
        gEditor.getCaretPosition().col == 44 + token.length,
       "The editor didn't jump to the correct token. (5)");
 
-    EventUtils.sendKey("UP");
+    EventUtils.sendKey("UP", gDebugger);
     ok(gEditor.getCaretPosition().line == 19 &&
        gEditor.getCaretPosition().col == 4 + token.length,
       "The editor didn't jump to the correct token. (5.1)");
 
 
     token = "debugger;";
     write(":bogus#" + token);
     ok(gEditor.getCaretPosition().line == 8 &&
@@ -222,24 +222,24 @@ function testScriptSearching() {
     write("#" + token);
     ok(gEditor.getCaretPosition().line == 2 &&
        gEditor.getCaretPosition().col == 44 + token.length,
       "The editor didn't jump to the correct token. (12.1)");
     isnot(gMenulist.getAttribute("label"), noMatchingScripts,
       "The menulist should not display a notice that matches are found.");
 
     clear();
-    EventUtils.sendKey("RETURN");
+    EventUtils.sendKey("RETURN", gDebugger);
     ok(gEditor.getCaretPosition().line == 2 &&
        gEditor.getCaretPosition().col == 44 + token.length,
       "The editor shouldn't jump to another token. (12.2)");
     isnot(gMenulist.getAttribute("label"), noMatchingScripts,
       "The menulist should not display a notice that matches are found.");
 
-    EventUtils.sendKey("ENTER");
+    EventUtils.sendKey("ENTER", gDebugger);
     ok(gEditor.getCaretPosition().line == 2 &&
        gEditor.getCaretPosition().col == 44 + token.length,
       "The editor shouldn't jump to another token. (12.3)");
     isnot(gMenulist.getAttribute("label"), noMatchingScripts,
       "The menulist should not display a notice that matches are found.");
 
 
     write(":1:2:3:a:b:c:::12");
@@ -247,37 +247,37 @@ function testScriptSearching() {
        gEditor.getCaretPosition().col == 0,
       "The editor didn't jump to the correct line. (13)");
 
     write("#don't#find#me#instead#find#" + token);
     ok(gEditor.getCaretPosition().line == 2 &&
        gEditor.getCaretPosition().col == 44 + token.length,
       "The editor didn't jump to the correct token. (14)");
 
-    EventUtils.sendKey("DOWN");
+    EventUtils.sendKey("DOWN", gDebugger);
     ok(gEditor.getCaretPosition().line == 8 &&
        gEditor.getCaretPosition().col == 2 + token.length,
       "The editor didn't jump to the correct token. (15)");
 
-    EventUtils.sendKey("DOWN");
+    EventUtils.sendKey("DOWN", gDebugger);
     ok(gEditor.getCaretPosition().line == 12 &&
        gEditor.getCaretPosition().col == 8 + token.length,
       "The editor didn't jump to the correct token. (16)");
 
-    EventUtils.sendKey("RETURN");
+    EventUtils.sendKey("RETURN", gDebugger);
     ok(gEditor.getCaretPosition().line == 19 &&
        gEditor.getCaretPosition().col == 4 + token.length,
       "The editor didn't jump to the correct token. (17)");
 
-    EventUtils.sendKey("ENTER");
+    EventUtils.sendKey("ENTER", gDebugger);
     ok(gEditor.getCaretPosition().line == 2 &&
        gEditor.getCaretPosition().col == 44 + token.length,
       "The editor didn't jump to the correct token. (18)");
 
-    EventUtils.sendKey("UP");
+    EventUtils.sendKey("UP", gDebugger);
     ok(gEditor.getCaretPosition().line == 19 &&
        gEditor.getCaretPosition().col == 4 + token.length,
       "The editor didn't jump to the correct token. (18.1)");
 
 
     clear();
     ok(gEditor.getCaretPosition().line == 19 &&
        gEditor.getCaretPosition().col == 4 + token.length,
@@ -300,17 +300,17 @@ function write(text) {
   clear();
   append(text);
 }
 
 function append(text) {
   gSearchBox.focus();
 
   for (let i = 0; i < text.length; i++) {
-    EventUtils.sendChar(text[i]);
+    EventUtils.sendChar(text[i], gDebugger);
   }
   info("Editor caret position: " + gEditor.getCaretPosition().toSource() + "\n");
 }
 
 registerCleanupFunction(function() {
   removeTab(gTab);
   gPane = null;
   gTab = null;
--- a/browser/devtools/debugger/test/browser_dbg_scripts-searching-02.js
+++ b/browser/devtools/debugger/test/browser_dbg_scripts-searching-02.js
@@ -23,33 +23,33 @@ function test()
   let framesAdded = false;
 
   debug_tab_pane(TAB_URL, function(aTab, aDebuggee, aPane) {
     gTab = aTab;
     gDebuggee = aDebuggee;
     gPane = aPane;
     gDebugger = gPane.panelWin;
 
+    gDebugger.addEventListener("Debugger:SourceShown", function _onEvent(aEvent) {
+      let url = aEvent.detail.url;
+      if (url.indexOf("-02.js") != -1) {
+        scriptShown = true;
+        gDebugger.removeEventListener(aEvent.type, _onEvent);
+        runTest();
+      }
+    });
+
     gDebugger.DebuggerController.activeThread.addOneTimeListener("framesadded", function() {
       framesAdded = true;
       runTest();
     });
 
     gDebuggee.firstCall();
   });
 
-  window.addEventListener("Debugger:SourceShown", function _onEvent(aEvent) {
-    let url = aEvent.detail.url;
-    if (url.indexOf("-02.js") != -1) {
-      scriptShown = true;
-      window.removeEventListener(aEvent.type, _onEvent);
-      runTest();
-    }
-  });
-
   function runTest()
   {
     if (scriptShown && framesAdded) {
       Services.tm.currentThread.dispatch({ run: testScriptSearching }, 0);
     }
   }
 }
 
@@ -60,23 +60,23 @@ function testScriptSearching() {
     gSearchBox = gDebugger.DebuggerView.Filtering._searchbox;
     gMenulist = gScripts._container;
 
     firstSearch();
   });
 }
 
 function firstSearch() {
-  window.addEventListener("Debugger:SourceShown", function _onEvent(aEvent) {
+  gDebugger.addEventListener("Debugger:SourceShown", function _onEvent(aEvent) {
     info("Current script url:\n" + aEvent.detail.url + "\n");
     info("Debugger editor text:\n" + gEditor.getText() + "\n");
 
     let url = aEvent.detail.url;
     if (url.indexOf("-01.js") != -1) {
-      window.removeEventListener(aEvent.type, _onEvent);
+      gDebugger.removeEventListener(aEvent.type, _onEvent);
 
       executeSoon(function() {
         info("Editor caret position: " + gEditor.getCaretPosition().toSource() + "\n");
         ok(gEditor.getCaretPosition().line == 4 &&
            gEditor.getCaretPosition().col == 0,
           "The editor didn't jump to the correct line. (1)");
         is(gScripts.visibleItems.length, 1,
           "Not all the correct scripts are shown after the search. (1)");
@@ -86,23 +86,23 @@ function firstSearch() {
     }
   });
   write(".*-01\.js:5");
 }
 
 function secondSearch() {
   let token = "deb";
 
-  window.addEventListener("Debugger:SourceShown", function _onEvent(aEvent) {
+  gDebugger.addEventListener("Debugger:SourceShown", function _onEvent(aEvent) {
     info("Current script url:\n" + aEvent.detail.url + "\n");
     info("Debugger editor text:\n" + gEditor.getText() + "\n");
 
     let url = aEvent.detail.url;
     if (url.indexOf("-02.js") != -1) {
-      window.removeEventListener(aEvent.type, _onEvent);
+      gDebugger.removeEventListener(aEvent.type, _onEvent);
 
       executeSoon(function() {
         append("#" + token);
 
         info("Editor caret position: " + gEditor.getCaretPosition().toSource() + "\n");
         ok(gEditor.getCaretPosition().line == 5 &&
            gEditor.getCaretPosition().col == 8 + token.length,
           "The editor didn't jump to the correct line. (2)");
@@ -112,42 +112,42 @@ function secondSearch() {
         waitForFirstScript();
       });
     }
   });
   gScripts.selectedIndex = 1;
 }
 
 function waitForFirstScript() {
-  window.addEventListener("Debugger:SourceShown", function _onEvent(aEvent) {
+  gDebugger.addEventListener("Debugger:SourceShown", function _onEvent(aEvent) {
     info("Current script url:\n" + aEvent.detail.url + "\n");
     info("Debugger editor text:\n" + gEditor.getText() + "\n");
 
     let url = aEvent.detail.url;
     if (url.indexOf("-01.js") != -1) {
-      window.removeEventListener(aEvent.type, _onEvent);
+      gDebugger.removeEventListener(aEvent.type, _onEvent);
 
       executeSoon(function() {
         thirdSearch();
       });
     }
   });
   gScripts.selectedIndex = 0;
 }
 
 function thirdSearch() {
   let token = "deb";
 
-  window.addEventListener("Debugger:SourceShown", function _onEvent(aEvent) {
+  gDebugger.addEventListener("Debugger:SourceShown", function _onEvent(aEvent) {
     info("Current script url:\n" + aEvent.detail.url + "\n");
     info("Debugger editor text:\n" + gEditor.getText() + "\n");
 
     let url = aEvent.detail.url;
     if (url.indexOf("-02.js") != -1) {
-      window.removeEventListener(aEvent.type, _onEvent);
+      gDebugger.removeEventListener(aEvent.type, _onEvent);
 
       executeSoon(function() {
         info("Editor caret position: " + gEditor.getCaretPosition().toSource() + "\n");
         ok(gEditor.getCaretPosition().line == 5 &&
            gEditor.getCaretPosition().col == 8 + token.length,
           "The editor didn't jump to the correct line. (3)");
         is(gScripts.visibleItems.length, 1,
           "Not all the correct scripts are shown after the search. (3)");
@@ -162,17 +162,17 @@ function thirdSearch() {
 function fourthSearch(i, string, token) {
   info("Searchbox value: " + gSearchBox.value);
 
   ok(gEditor.getCaretPosition().line == 5 &&
      gEditor.getCaretPosition().col == 8 + token.length + i,
     "The editor didn't remain at the correct token. (4)");
 
   if (string[i]) {
-    EventUtils.sendChar(string[i]);
+    EventUtils.sendChar(string[i], gDebugger);
     fourthSearch(i + 1, string, token);
     return;
   }
 
   clear();
   ok(gEditor.getCaretPosition().line == 5 &&
      gEditor.getCaretPosition().col == 8 + token.length + i,
     "The editor didn't remain at the correct token. (5)");
@@ -240,17 +240,17 @@ function write(text) {
   clear();
   append(text);
 }
 
 function append(text) {
   gSearchBox.focus();
 
   for (let i = 0; i < text.length; i++) {
-    EventUtils.sendChar(text[i]);
+    EventUtils.sendChar(text[i], gDebugger);
   }
   info("Editor caret position: " + gEditor.getCaretPosition().toSource() + "\n");
 }
 
 registerCleanupFunction(function() {
   removeTab(gTab);
   gPane = null;
   gTab = null;
--- a/browser/devtools/debugger/test/browser_dbg_scripts-searching-03.js
+++ b/browser/devtools/debugger/test/browser_dbg_scripts-searching-03.js
@@ -25,33 +25,33 @@ function test()
 
   debug_tab_pane(TAB_URL, function(aTab, aDebuggee, aPane) {
     gTab = aTab;
     gDebuggee = aDebuggee;
     gPane = aPane;
     gDebugger = gPane.panelWin;
     gDebugger.SourceResults.prototype.alwaysExpand = false;
 
+    gDebugger.addEventListener("Debugger:SourceShown", function _onEvent(aEvent) {
+      let url = aEvent.detail.url;
+      if (url.indexOf("-02.js") != -1) {
+        scriptShown = true;
+        gDebugger.removeEventListener(aEvent.type, _onEvent);
+        runTest();
+      }
+    });
+
     gDebugger.DebuggerController.activeThread.addOneTimeListener("framesadded", function() {
       framesAdded = true;
       runTest();
     });
 
     gDebuggee.firstCall();
   });
 
-  window.addEventListener("Debugger:SourceShown", function _onEvent(aEvent) {
-    let url = aEvent.detail.url;
-    if (url.indexOf("-02.js") != -1) {
-      scriptShown = true;
-      window.removeEventListener(aEvent.type, _onEvent);
-      runTest();
-    }
-  });
-
   function runTest()
   {
     if (scriptShown && framesAdded) {
       Services.tm.currentThread.dispatch({ run: testScriptSearching }, 0);
     }
   }
 }
 
@@ -69,18 +69,18 @@ function testScriptSearching() {
 function firstSearch() {
   is(gSearchView._container._list.childNodes.length, 0,
     "The global search pane shouldn't have any child nodes yet.");
   is(gSearchView._container._parent.hidden, true,
     "The global search pane shouldn't be visible yet.");
   is(gSearchView._splitter.hidden, true,
     "The global search pane splitter shouldn't be visible yet.");
 
-  window.addEventListener("Debugger:GlobalSearch:MatchFound", function _onEvent(aEvent) {
-    window.removeEventListener(aEvent.type, _onEvent);
+  gDebugger.addEventListener("Debugger:GlobalSearch:MatchFound", function _onEvent(aEvent) {
+    gDebugger.removeEventListener(aEvent.type, _onEvent);
     info("Current script url:\n" + gScripts.selectedValue + "\n");
     info("Debugger editor text:\n" + gEditor.getText() + "\n");
 
     let url = gScripts.selectedValue;
     if (url.indexOf("-02.js") != -1) {
       executeSoon(function() {
         info("Editor caret position: " + gEditor.getCaretPosition().toSource() + "\n");
         ok(gEditor.getCaretPosition().line == 5 &&
@@ -185,18 +185,18 @@ function firstSearch() {
 function secondSearch() {
   isnot(gSearchView._container._list.childNodes.length, 0,
     "The global search pane should have some child nodes from the previous search.");
   is(gSearchView._container._parent.hidden, false,
     "The global search pane should be visible from the previous search.");
   is(gSearchView._splitter.hidden, false,
     "The global search pane splitter should be visible from the previous search.");
 
-  window.addEventListener("Debugger:GlobalSearch:MatchFound", function _onEvent(aEvent) {
-    window.removeEventListener(aEvent.type, _onEvent);
+  gDebugger.addEventListener("Debugger:GlobalSearch:MatchFound", function _onEvent(aEvent) {
+    gDebugger.removeEventListener(aEvent.type, _onEvent);
     info("Current script url:\n" + gScripts.selectedValue + "\n");
     info("Debugger editor text:\n" + gEditor.getText() + "\n");
 
     let url = gScripts.selectedValue;
     if (url.indexOf("-02.js") != -1) {
       executeSoon(function() {
         info("Editor caret position: " + gEditor.getCaretPosition().toSource() + "\n");
         ok(gEditor.getCaretPosition().line == 5 &&
@@ -306,25 +306,25 @@ function clear() {
 
 function write(text) {
   clear();
   append(text);
 }
 
 function backspace(times) {
   for (let i = 0; i < times; i++) {
-    EventUtils.sendKey("BACK_SPACE")
+    EventUtils.sendKey("BACK_SPACE", gDebugger);
   }
 }
 
 function append(text) {
   gSearchBox.focus();
 
   for (let i = 0; i < text.length; i++) {
-    EventUtils.sendChar(text[i]);
+    EventUtils.sendChar(text[i], gDebugger);
   }
   info("Editor caret position: " + gEditor.getCaretPosition().toSource() + "\n");
 }
 
 registerCleanupFunction(function() {
   removeTab(gTab);
   gPane = null;
   gTab = null;
--- a/browser/devtools/debugger/test/browser_dbg_scripts-searching-04.js
+++ b/browser/devtools/debugger/test/browser_dbg_scripts-searching-04.js
@@ -25,33 +25,33 @@ function test()
 
   debug_tab_pane(TAB_URL, function(aTab, aDebuggee, aPane) {
     gTab = aTab;
     gDebuggee = aDebuggee;
     gPane = aPane;
     gDebugger = gPane.panelWin;
     gDebugger.SourceResults.prototype.alwaysExpand = false;
 
+    gDebugger.addEventListener("Debugger:SourceShown", function _onEvent(aEvent) {
+      let url = aEvent.detail.url;
+      if (url.indexOf("-02.js") != -1) {
+        scriptShown = true;
+        gDebugger.removeEventListener(aEvent.type, _onEvent);
+        runTest();
+      }
+    });
+
     gDebugger.DebuggerController.activeThread.addOneTimeListener("framesadded", function() {
       framesAdded = true;
       runTest();
     });
 
     gDebuggee.firstCall();
   });
 
-  window.addEventListener("Debugger:SourceShown", function _onEvent(aEvent) {
-    let url = aEvent.detail.url;
-    if (url.indexOf("-02.js") != -1) {
-      scriptShown = true;
-      window.removeEventListener(aEvent.type, _onEvent);
-      runTest();
-    }
-  });
-
   function runTest()
   {
     if (scriptShown && framesAdded) {
       Services.tm.currentThread.dispatch({ run: testScriptSearching }, 0);
     }
   }
 }
 
@@ -69,18 +69,18 @@ function testScriptSearching() {
 function doSearch() {
   is(gSearchView._container._list.childNodes.length, 0,
     "The global search pane shouldn't have any child nodes yet.");
   is(gSearchView._container._parent.hidden, true,
     "The global search pane shouldn't be visible yet.");
   is(gSearchView._splitter.hidden, true,
     "The global search pane splitter shouldn't be visible yet.");
 
-  window.addEventListener("Debugger:GlobalSearch:MatchFound", function _onEvent(aEvent) {
-    window.removeEventListener(aEvent.type, _onEvent);
+  gDebugger.addEventListener("Debugger:GlobalSearch:MatchFound", function _onEvent(aEvent) {
+    gDebugger.removeEventListener(aEvent.type, _onEvent);
     info("Current script url:\n" + gScripts.selectedValue + "\n");
     info("Debugger editor text:\n" + gEditor.getText() + "\n");
 
     let url = gScripts.selectedValue;
     if (url.indexOf("-02.js") != -1) {
       executeSoon(function() {
         info("Editor caret position: " + gEditor.getCaretPosition().toSource() + "\n");
         ok(gEditor.getCaretPosition().line == 5 &&
@@ -103,18 +103,18 @@ function doSearch() {
     }
   });
   executeSoon(function() {
     write("!eval");
   });
 }
 
 function doFirstJump() {
-  window.addEventListener("Debugger:SourceShown", function _onEvent(aEvent) {
-    window.removeEventListener(aEvent.type, _onEvent);
+  gDebugger.addEventListener("Debugger:SourceShown", function _onEvent(aEvent) {
+    gDebugger.removeEventListener(aEvent.type, _onEvent);
     info("Current script url:\n" + aEvent.detail.url + "\n");
     info("Debugger editor text:\n" + gEditor.getText() + "\n");
 
     let url = aEvent.detail.url;
     if (url.indexOf("-01.js") != -1) {
       executeSoon(function() {
         info("Editor caret position: " + gEditor.getCaretPosition().toSource() + "\n");
         ok(gEditor.getCaretPosition().line == 4 &&
@@ -125,23 +125,23 @@ function doFirstJump() {
 
         doSecondJump();
       });
     } else {
       ok(false, "We jumped in a bowl of hot lava (aka WRONG MATCH). That was bad for us.");
     }
   });
   executeSoon(function() {
-    EventUtils.sendKey("DOWN");
+    EventUtils.sendKey("DOWN", gDebugger);
   });
 }
 
 function doSecondJump() {
-  window.addEventListener("Debugger:SourceShown", function _onEvent(aEvent) {
-    window.removeEventListener(aEvent.type, _onEvent);
+  gDebugger.addEventListener("Debugger:SourceShown", function _onEvent(aEvent) {
+    gDebugger.removeEventListener(aEvent.type, _onEvent);
     info("Current script url:\n" + aEvent.detail.url + "\n");
     info("Debugger editor text:\n" + gEditor.getText() + "\n");
 
     let url = aEvent.detail.url;
     if (url.indexOf("-02.js") != -1) {
       executeSoon(function() {
         info("Editor caret position: " + gEditor.getCaretPosition().toSource() + "\n");
         ok(gEditor.getCaretPosition().line == 5 &&
@@ -152,23 +152,23 @@ function doSecondJump() {
 
         doWrapAroundJump();
       });
     } else {
       ok(false, "We jumped in a bowl of hot lava (aka WRONG MATCH). That was bad for us.");
     }
   });
   executeSoon(function() {
-    EventUtils.sendKey("RETURN");
+    EventUtils.sendKey("RETURN", gDebugger);
   });
 }
 
 function doWrapAroundJump() {
-  window.addEventListener("Debugger:SourceShown", function _onEvent(aEvent) {
-    window.removeEventListener(aEvent.type, _onEvent);
+  gDebugger.addEventListener("Debugger:SourceShown", function _onEvent(aEvent) {
+    gDebugger.removeEventListener(aEvent.type, _onEvent);
     info("Current script url:\n" + aEvent.detail.url + "\n");
     info("Debugger editor text:\n" + gEditor.getText() + "\n");
 
     let url = aEvent.detail.url;
     if (url.indexOf("-01.js") != -1) {
       executeSoon(function() {
         info("Editor caret position: " + gEditor.getCaretPosition().toSource() + "\n");
         ok(gEditor.getCaretPosition().line == 4 &&
@@ -179,23 +179,23 @@ function doWrapAroundJump() {
 
         doBackwardsWrapAroundJump();
       });
     } else {
       ok(false, "We jumped in a bowl of hot lava (aka WRONG MATCH). That was bad for us.");
     }
   });
   executeSoon(function() {
-    EventUtils.sendKey("ENTER");
+    EventUtils.sendKey("ENTER", gDebugger);
   });
 }
 
 function doBackwardsWrapAroundJump() {
-  window.addEventListener("Debugger:SourceShown", function _onEvent(aEvent) {
-    window.removeEventListener(aEvent.type, _onEvent);
+  gDebugger.addEventListener("Debugger:SourceShown", function _onEvent(aEvent) {
+    gDebugger.removeEventListener(aEvent.type, _onEvent);
     info("Current script url:\n" + aEvent.detail.url + "\n");
     info("Debugger editor text:\n" + gEditor.getText() + "\n");
 
     let url = aEvent.detail.url;
     if (url.indexOf("-02.js") != -1) {
       executeSoon(function() {
         info("Editor caret position: " + gEditor.getCaretPosition().toSource() + "\n");
         ok(gEditor.getCaretPosition().line == 5 &&
@@ -206,23 +206,23 @@ function doBackwardsWrapAroundJump() {
 
         testSearchTokenEmpty();
       });
     } else {
       ok(false, "We jumped in a bowl of hot lava (aka WRONG MATCH). That was bad for us.");
     }
   });
   executeSoon(function() {
-    EventUtils.sendKey("UP");
+    EventUtils.sendKey("UP", gDebugger);
   });
 }
 
 function testSearchTokenEmpty() {
-  window.addEventListener("Debugger:GlobalSearch:TokenEmpty", function _onEvent(aEvent) {
-    window.removeEventListener(aEvent.type, _onEvent);
+  gDebugger.addEventListener("Debugger:GlobalSearch:TokenEmpty", function _onEvent(aEvent) {
+    gDebugger.removeEventListener(aEvent.type, _onEvent);
     info("Current script url:\n" + gScripts.selectedValue + "\n");
     info("Debugger editor text:\n" + gEditor.getText() + "\n");
 
     let url = gScripts.selectedValue;
     if (url.indexOf("-02.js") != -1) {
       executeSoon(function() {
         info("Editor caret position: " + gEditor.getCaretPosition().toSource() + "\n");
         ok(gEditor.getCaretPosition().line == 5 &&
@@ -254,25 +254,25 @@ function clear() {
 
 function write(text) {
   clear();
   append(text);
 }
 
 function backspace(times) {
   for (let i = 0; i < times; i++) {
-    EventUtils.sendKey("BACK_SPACE")
+    EventUtils.sendKey("BACK_SPACE", gDebugger);
   }
 }
 
 function append(text) {
   gSearchBox.focus();
 
   for (let i = 0; i < text.length; i++) {
-    EventUtils.sendChar(text[i]);
+    EventUtils.sendChar(text[i], gDebugger);
   }
   info("Editor caret position: " + gEditor.getCaretPosition().toSource() + "\n");
 }
 
 registerCleanupFunction(function() {
   removeTab(gTab);
   gPane = null;
   gTab = null;
--- a/browser/devtools/debugger/test/browser_dbg_scripts-searching-05.js
+++ b/browser/devtools/debugger/test/browser_dbg_scripts-searching-05.js
@@ -25,33 +25,33 @@ function test()
 
   debug_tab_pane(TAB_URL, function(aTab, aDebuggee, aPane) {
     gTab = aTab;
     gDebuggee = aDebuggee;
     gPane = aPane;
     gDebugger = gPane.panelWin;
     gDebugger.SourceResults.prototype.alwaysExpand = false;
 
+    gDebugger.addEventListener("Debugger:SourceShown", function _onEvent(aEvent) {
+      let url = aEvent.detail.url;
+      if (url.indexOf("-02.js") != -1) {
+        scriptShown = true;
+        gDebugger.removeEventListener(aEvent.type, _onEvent);
+        runTest();
+      }
+    });
+
     gDebugger.DebuggerController.activeThread.addOneTimeListener("framesadded", function() {
       framesAdded = true;
       runTest();
     });
 
     gDebuggee.firstCall();
   });
 
-  window.addEventListener("Debugger:SourceShown", function _onEvent(aEvent) {
-    let url = aEvent.detail.url;
-    if (url.indexOf("-02.js") != -1) {
-      scriptShown = true;
-      window.removeEventListener(aEvent.type, _onEvent);
-      runTest();
-    }
-  });
-
   function runTest()
   {
     if (scriptShown && framesAdded) {
       Services.tm.currentThread.dispatch({ run: testScriptSearching }, 0);
     }
   }
 }
 
@@ -69,18 +69,18 @@ function testScriptSearching() {
 function doSearch() {
   is(gSearchView._container._list.childNodes.length, 0,
     "The global search pane shouldn't have any child nodes yet.");
   is(gSearchView._container._parent.hidden, true,
     "The global search pane shouldn't be visible yet.");
   is(gSearchView._splitter.hidden, true,
     "The global search pane splitter shouldn't be visible yet.");
 
-  window.addEventListener("Debugger:GlobalSearch:MatchFound", function _onEvent(aEvent) {
-    window.removeEventListener(aEvent.type, _onEvent);
+  gDebugger.addEventListener("Debugger:GlobalSearch:MatchFound", function _onEvent(aEvent) {
+    gDebugger.removeEventListener(aEvent.type, _onEvent);
     info("Current script url:\n" + gScripts.selectedValue + "\n");
     info("Debugger editor text:\n" + gEditor.getText() + "\n");
 
     let url = gScripts.selectedValue;
     if (url.indexOf("-02.js") != -1) {
       executeSoon(function() {
         info("Editor caret position: " + gEditor.getCaretPosition().toSource() + "\n");
         ok(gEditor.getCaretPosition().line == 5 &&
@@ -113,32 +113,32 @@ function testLocationChange()
   let cacheCleared = false;
 
   function _maybeFinish() {
     if (viewCleared && cacheCleared) {
       closeDebuggerAndFinish();
     }
   }
 
-  window.addEventListener("Debugger:GlobalSearch:ViewCleared", function _onViewCleared(aEvent) {
-    window.removeEventListener(aEvent.type, _onViewCleared);
+  gDebugger.addEventListener("Debugger:GlobalSearch:ViewCleared", function _onViewCleared(aEvent) {
+    gDebugger.removeEventListener(aEvent.type, _onViewCleared);
 
     is(gSearchView._container._list.childNodes.length, 0,
       "The global search pane shouldn't have any child nodes after a page navigation.");
     is(gSearchView._container._parent.hidden, true,
       "The global search pane shouldn't be visible after a page navigation.");
     is(gSearchView._splitter.hidden, true,
       "The global search pane splitter shouldn't be visible after a page navigation.");
 
     viewCleared = true;
     _maybeFinish();
   });
 
-  window.addEventListener("Debugger:GlobalSearch:CacheCleared", function _onCacheCleared(aEvent) {
-    window.removeEventListener(aEvent.type, _onCacheCleared);
+  gDebugger.addEventListener("Debugger:GlobalSearch:CacheCleared", function _onCacheCleared(aEvent) {
+    gDebugger.removeEventListener(aEvent.type, _onCacheCleared);
 
     is(gSearchView._cache.size, 0,
       "The scripts sources cache for global searching should be cleared after a page navigation.")
 
     cacheCleared = true;
     _maybeFinish();
   });
 
@@ -154,17 +154,17 @@ function write(text) {
   clear();
   append(text);
 }
 
 function append(text) {
   gSearchBox.focus();
 
   for (let i = 0; i < text.length; i++) {
-    EventUtils.sendChar(text[i]);
+    EventUtils.sendChar(text[i], gDebugger);
   }
   info("Editor caret position: " + gEditor.getCaretPosition().toSource() + "\n");
 }
 
 registerCleanupFunction(function() {
   removeTab(gTab);
   gPane = null;
   gTab = null;
--- a/browser/devtools/debugger/test/browser_dbg_scripts-searching-06.js
+++ b/browser/devtools/debugger/test/browser_dbg_scripts-searching-06.js
@@ -25,33 +25,33 @@ function test()
 
   debug_tab_pane(TAB_URL, function(aTab, aDebuggee, aPane) {
     gTab = aTab;
     gDebuggee = aDebuggee;
     gPane = aPane;
     gDebugger = gPane.panelWin;
     gDebugger.SourceResults.prototype.alwaysExpand = false;
 
+    gDebugger.addEventListener("Debugger:SourceShown", function _onEvent(aEvent) {
+      let url = aEvent.detail.url;
+      if (url.indexOf("-02.js") != -1) {
+        scriptShown = true;
+        gDebugger.removeEventListener(aEvent.type, _onEvent);
+        runTest();
+      }
+    });
+
     gDebugger.DebuggerController.activeThread.addOneTimeListener("framesadded", function() {
       framesAdded = true;
       runTest();
     });
 
     gDebuggee.firstCall();
   });
 
-  window.addEventListener("Debugger:SourceShown", function _onEvent(aEvent) {
-    let url = aEvent.detail.url;
-    if (url.indexOf("-02.js") != -1) {
-      scriptShown = true;
-      window.removeEventListener(aEvent.type, _onEvent);
-      runTest();
-    }
-  });
-
   function runTest()
   {
     if (scriptShown && framesAdded) {
       Services.tm.currentThread.dispatch({ run: testScriptSearching }, 0);
     }
   }
 }
 
@@ -62,18 +62,18 @@ function testScriptSearching() {
     gSearchView = gDebugger.DebuggerView.GlobalSearch;
     gSearchBox = gDebugger.DebuggerView.Filtering._searchbox;
 
     doSearch();
   });
 }
 
 function doSearch() {
-  window.addEventListener("Debugger:GlobalSearch:MatchFound", function _onEvent(aEvent) {
-    window.removeEventListener(aEvent.type, _onEvent);
+  gDebugger.addEventListener("Debugger:GlobalSearch:MatchFound", function _onEvent(aEvent) {
+    gDebugger.removeEventListener(aEvent.type, _onEvent);
     info("Current script url:\n" + gScripts.selectedValue + "\n");
     info("Debugger editor text:\n" + gEditor.getText() + "\n");
 
     let url = gScripts.selectedValue;
     if (url.indexOf("-02.js") != -1) {
       executeSoon(function() {
         info("Editor caret position: " + gEditor.getCaretPosition().toSource() + "\n");
         ok(gEditor.getCaretPosition().line == 5 &&
@@ -89,18 +89,18 @@ function doSearch() {
     }
   });
   executeSoon(function() {
     write("!eval");
   });
 }
 
 function testSearchMatchNotFound() {
-  window.addEventListener("Debugger:GlobalSearch:MatchNotFound", function _onEvent(aEvent) {
-    window.removeEventListener(aEvent.type, _onEvent);
+  gDebugger.addEventListener("Debugger:GlobalSearch:MatchNotFound", function _onEvent(aEvent) {
+    gDebugger.removeEventListener(aEvent.type, _onEvent);
     info("Current script url:\n" + gScripts.selectedValue + "\n");
     info("Debugger editor text:\n" + gEditor.getText() + "\n");
 
     let url = gScripts.selectedValue;
     if (url.indexOf("-02.js") != -1) {
       executeSoon(function() {
         info("Editor caret position: " + gEditor.getCaretPosition().toSource() + "\n");
         ok(gEditor.getCaretPosition().line == 5 &&
@@ -127,17 +127,17 @@ function write(text) {
   clear();
   append(text);
 }
 
 function append(text) {
   gSearchBox.focus();
 
   for (let i = 0; i < text.length; i++) {
-    EventUtils.sendChar(text[i]);
+    EventUtils.sendChar(text[i], gDebugger);
   }
   info("Editor caret position: " + gEditor.getCaretPosition().toSource() + "\n");
 }
 
 registerCleanupFunction(function() {
   removeTab(gTab);
   gPane = null;
   gTab = null;
--- a/browser/devtools/debugger/test/browser_dbg_scripts-searching-07.js
+++ b/browser/devtools/debugger/test/browser_dbg_scripts-searching-07.js
@@ -26,33 +26,33 @@ function test()
 
   debug_tab_pane(TAB_URL, function(aTab, aDebuggee, aPane) {
     gTab = aTab;
     gDebuggee = aDebuggee;
     gPane = aPane;
     gDebugger = gPane.panelWin;
     gDebugger.SourceResults.prototype.alwaysExpand = false;
 
+    gDebugger.addEventListener("Debugger:SourceShown", function _onEvent(aEvent) {
+      let url = aEvent.detail.url;
+      if (url.indexOf("-02.js") != -1) {
+        scriptShown = true;
+        gDebugger.removeEventListener(aEvent.type, _onEvent);
+        runTest();
+      }
+    });
+
     gDebugger.DebuggerController.activeThread.addOneTimeListener("framesadded", function() {
       framesAdded = true;
       runTest();
     });
 
     gDebuggee.firstCall();
   });
 
-  window.addEventListener("Debugger:SourceShown", function _onEvent(aEvent) {
-    let url = aEvent.detail.url;
-    if (url.indexOf("-02.js") != -1) {
-      scriptShown = true;
-      window.removeEventListener(aEvent.type, _onEvent);
-      runTest();
-    }
-  });
-
   function runTest()
   {
     if (scriptShown && framesAdded) {
       Services.tm.currentThread.dispatch({ run: testScriptSearching }, 0);
     }
   }
 }
 
@@ -63,18 +63,18 @@ function testScriptSearching() {
     gSearchView = gDebugger.DebuggerView.GlobalSearch;
     gSearchBox = gDebugger.DebuggerView.Filtering._searchbox;
 
     doSearch();
   });
 }
 
 function doSearch() {
-  window.addEventListener("Debugger:GlobalSearch:MatchFound", function _onEvent(aEvent) {
-    window.removeEventListener(aEvent.type, _onEvent);
+  gDebugger.addEventListener("Debugger:GlobalSearch:MatchFound", function _onEvent(aEvent) {
+    gDebugger.removeEventListener(aEvent.type, _onEvent);
     info("Current script url:\n" + gScripts.selectedValue + "\n");
     info("Debugger editor text:\n" + gEditor.getText() + "\n");
 
     let url = gScripts.selectedValue;
     if (url.indexOf("-02.js") != -1) {
       executeSoon(function() {
         continueTest();
       });
@@ -165,18 +165,18 @@ function testClickLineToJump(scriptResul
   let targetResults = scriptResults[0];
   let firstHeader = targetResults.querySelector(".dbg-results-header");
   let firstHeaderItem = gDebugger.SourceResults.getItemForElement(firstHeader);
   firstHeaderItem.instance.expand()
 
   is(firstHeaderItem.instance.expanded, true,
     "The first script results should be expanded after direct function call.");
 
-  window.addEventListener("Debugger:SourceShown", function _onEvent(aEvent) {
-    window.removeEventListener(aEvent.type, _onEvent);
+  gDebugger.addEventListener("Debugger:SourceShown", function _onEvent(aEvent) {
+    gDebugger.removeEventListener(aEvent.type, _onEvent);
     info("Current script url:\n" + aEvent.detail.url + "\n");
     info("Debugger editor text:\n" + gEditor.getText() + "\n");
 
     let url = aEvent.detail.url;
     if (url.indexOf("-01.js") != -1) {
       executeSoon(function() {
         info("Editor caret position: " + gEditor.getCaretPosition().toSource() + "\n");
         ok(gEditor.getCaretPosition().line == 0 &&
@@ -200,18 +200,18 @@ function testClickMatchToJump(scriptResu
   let targetResults = scriptResults[1];
   let secondHeader = targetResults.querySelector(".dbg-results-header");
   let secondHeaderItem = gDebugger.SourceResults.getItemForElement(secondHeader);
   secondHeaderItem.instance.expand()
 
   is(secondHeaderItem.instance.expanded, true,
     "The second script results should be expanded after direct function call.");
 
-  window.addEventListener("Debugger:SourceShown", function _onEvent(aEvent) {
-    window.removeEventListener(aEvent.type, _onEvent);
+  gDebugger.addEventListener("Debugger:SourceShown", function _onEvent(aEvent) {
+    gDebugger.removeEventListener(aEvent.type, _onEvent);
     info("Current script url:\n" + aEvent.detail.url + "\n");
     info("Debugger editor text:\n" + gEditor.getText() + "\n");
 
     let url = aEvent.detail.url;
     if (url.indexOf("-02.js") != -1) {
       executeSoon(function() {
         info("Editor caret position: " + gEditor.getCaretPosition().toSource() + "\n");
         ok(gEditor.getCaretPosition().line == 5 &&
@@ -241,17 +241,17 @@ function write(text) {
   clear();
   append(text);
 }
 
 function append(text) {
   gSearchBox.focus();
 
   for (let i = 0; i < text.length; i++) {
-    EventUtils.sendChar(text[i]);
+    EventUtils.sendChar(text[i], gDebugger);
   }
   info("Editor caret position: " + gEditor.getCaretPosition().toSource() + "\n");
 }
 
 registerCleanupFunction(function() {
   removeTab(gTab);
   gPane = null;
   gTab = null;
--- a/browser/devtools/debugger/test/browser_dbg_scripts-searching-08.js
+++ b/browser/devtools/debugger/test/browser_dbg_scripts-searching-08.js
@@ -25,33 +25,33 @@ function test()
 
   debug_tab_pane(TAB_URL, function(aTab, aDebuggee, aPane) {
     gTab = aTab;
     gDebuggee = aDebuggee;
     gPane = aPane;
     gDebugger = gPane.panelWin;
     gDebugger.SourceResults.prototype.alwaysExpand = false;
 
+    gDebugger.addEventListener("Debugger:SourceShown", function _onEvent(aEvent) {
+      let url = aEvent.detail.url;
+      if (url.indexOf("-02.js") != -1) {
+        scriptShown = true;
+        gDebugger.removeEventListener(aEvent.type, _onEvent);
+        runTest();
+      }
+    });
+
     gDebugger.DebuggerController.activeThread.addOneTimeListener("framesadded", function() {
       framesAdded = true;
       runTest();
     });
 
     gDebuggee.firstCall();
   });
 
-  window.addEventListener("Debugger:SourceShown", function _onEvent(aEvent) {
-    let url = aEvent.detail.url;
-    if (url.indexOf("-02.js") != -1) {
-      scriptShown = true;
-      window.removeEventListener(aEvent.type, _onEvent);
-      runTest();
-    }
-  });
-
   function runTest()
   {
     if (scriptShown && framesAdded) {
       Services.tm.currentThread.dispatch({ run: testScriptSearching }, 0);
     }
   }
 }
 
@@ -65,18 +65,18 @@ function testScriptSearching() {
     doSearch();
   });
 }
 
 function doSearch() {
   is(gSearchView._container._parent.hidden, true,
     "The global search pane shouldn't be visible yet.");
 
-  window.addEventListener("Debugger:GlobalSearch:MatchFound", function _onEvent(aEvent) {
-    window.removeEventListener(aEvent.type, _onEvent);
+  gDebugger.addEventListener("Debugger:GlobalSearch:MatchFound", function _onEvent(aEvent) {
+    gDebugger.removeEventListener(aEvent.type, _onEvent);
     info("Current script url:\n" + gScripts.selectedValue + "\n");
     info("Debugger editor text:\n" + gEditor.getText() + "\n");
 
     let url = gScripts.selectedValue;
     if (url.indexOf("-02.js") != -1) {
       executeSoon(function() {
         testFocusLost();
       });
@@ -89,18 +89,18 @@ function doSearch() {
   });
 }
 
 function testFocusLost()
 {
   is(gSearchView._container._parent.hidden, false,
     "The global search pane should be visible after a search.");
 
-  window.addEventListener("Debugger:GlobalSearch:ViewCleared", function _onEvent(aEvent) {
-    window.removeEventListener(aEvent.type, _onEvent);
+  gDebugger.addEventListener("Debugger:GlobalSearch:ViewCleared", function _onEvent(aEvent) {
+    gDebugger.removeEventListener(aEvent.type, _onEvent);
     info("Current script url:\n" + gScripts.selectedValue + "\n");
     info("Debugger editor text:\n" + gEditor.getText() + "\n");
 
     let url = gScripts.selectedValue;
     if (url.indexOf("-02.js") != -1) {
       executeSoon(function() {
         reshowSearch();
       });
@@ -112,18 +112,18 @@ function testFocusLost()
     gDebugger.DebuggerView.editor.focus();
   });
 }
 
 function reshowSearch() {
   is(gSearchView._container._parent.hidden, true,
     "The global search pane shouldn't be visible after the search was stopped.");
 
-  window.addEventListener("Debugger:GlobalSearch:MatchFound", function _onEvent(aEvent) {
-    window.removeEventListener(aEvent.type, _onEvent);
+  gDebugger.addEventListener("Debugger:GlobalSearch:MatchFound", function _onEvent(aEvent) {
+    gDebugger.removeEventListener(aEvent.type, _onEvent);
     info("Current script url:\n" + gScripts.selectedValue + "\n");
     info("Debugger editor text:\n" + gEditor.getText() + "\n");
 
     let url = gScripts.selectedValue;
     if (url.indexOf("-02.js") != -1) {
       executeSoon(function() {
         testEscape();
       });
@@ -136,18 +136,18 @@ function reshowSearch() {
   });
 }
 
 function testEscape()
 {
   is(gSearchView._container._parent.hidden, false,
     "The global search pane should be visible after a re-search.");
 
-  window.addEventListener("Debugger:GlobalSearch:ViewCleared", function _onEvent(aEvent) {
-    window.removeEventListener(aEvent.type, _onEvent);
+  gDebugger.addEventListener("Debugger:GlobalSearch:ViewCleared", function _onEvent(aEvent) {
+    gDebugger.removeEventListener(aEvent.type, _onEvent);
     info("Current script url:\n" + gScripts.selectedValue + "\n");
     info("Debugger editor text:\n" + gEditor.getText() + "\n");
 
     let url = gScripts.selectedValue;
     if (url.indexOf("-02.js") != -1) {
       executeSoon(function() {
         finalCheck();
       });
@@ -175,29 +175,29 @@ function clear() {
 
 function write(text) {
   clear();
   append(text);
 }
 
 function sendEnter() {
   gSearchBox.focus();
-  EventUtils.sendKey("ENTER");
+  EventUtils.sendKey("ENTER", gDebugger);
 }
 
 function sendEscape() {
   gSearchBox.focus();
-  EventUtils.sendKey("ESCAPE");
+  EventUtils.sendKey("ESCAPE", gDebugger);
 }
 
 function append(text) {
   gSearchBox.focus();
 
   for (let i = 0; i < text.length; i++) {
-    EventUtils.sendChar(text[i]);
+    EventUtils.sendChar(text[i], gDebugger);
   }
   info("Editor caret position: " + gEditor.getCaretPosition().toSource() + "\n");
 }
 
 registerCleanupFunction(function() {
   removeTab(gTab);
   gPane = null;
   gTab = null;
--- a/browser/devtools/debugger/test/browser_dbg_scripts-searching-files_ui.js
+++ b/browser/devtools/debugger/test/browser_dbg_scripts-searching-files_ui.js
@@ -20,37 +20,38 @@ var gMenulist = null;
 
 function test()
 {
   debug_tab_pane(TAB_URL, function(aTab, aDebuggee, aPane) {
     gTab = aTab;
     gDebuggee = aDebuggee;
     gPane = aPane;
     gDebugger = gPane.panelWin;
+
+    gDebugger.addEventListener("Debugger:SourceShown", function _onEvent(aEvent) {
+      gDebugger.removeEventListener(aEvent.type, _onEvent);
+      Services.tm.currentThread.dispatch({ run: testScriptSearching }, 0);
+    });
   });
 
-  window.addEventListener("Debugger:SourceShown", function _onEvent(aEvent) {
-    window.removeEventListener(aEvent.type, _onEvent);
-    Services.tm.currentThread.dispatch({ run: testScriptSearching }, 0);
-  });
 }
 
 function testScriptSearching() {
   gEditor = gDebugger.DebuggerView.editor;
   gScripts = gDebugger.DebuggerView.Sources;
   gSearchBox = gDebugger.DebuggerView.Filtering._searchbox;
   gFilteredSources = gDebugger.DebuggerView.FilteredSources;
   gMenulist = gScripts._container;
 
   firstSearch();
 }
 
 function firstSearch() {
-  window.addEventListener("popupshown", function _onEvent(aEvent) {
-    window.removeEventListener(aEvent.type, _onEvent);
+  gDebugger.addEventListener("popupshown", function _onEvent(aEvent) {
+    gDebugger.removeEventListener(aEvent.type, _onEvent);
     info("Current script url:\n" + gScripts.selectedValue + "\n");
     info("Debugger editor text:\n" + gEditor.getText() + "\n");
 
     is(gFilteredSources.totalItems, 3,
       "The filtered sources view should have 3 items available.");
     is(gFilteredSources.visibleItems.length, 3,
       "The filtered sources view should have 3 items visible.");
 
@@ -102,23 +103,23 @@ function firstSearch() {
   write(".");
 }
 
 function secondSearch() {
   let sourceshown = false;
   let popupshown = false;
   let proceeded = false;
 
-  window.addEventListener("Debugger:SourceShown", function _onEvent1(aEvent) {
-    window.removeEventListener(aEvent.type, _onEvent1);
+  gDebugger.addEventListener("Debugger:SourceShown", function _onEvent1(aEvent) {
+    gDebugger.removeEventListener(aEvent.type, _onEvent1);
     sourceshown = true;
     executeSoon(proceed);
   });
-  window.addEventListener("popupshown", function _onEvent2(aEvent) {
-    window.removeEventListener(aEvent.type, _onEvent2);
+  gDebugger.addEventListener("popupshown", function _onEvent2(aEvent) {
+    gDebugger.removeEventListener(aEvent.type, _onEvent2);
     popupshown = true;
     executeSoon(proceed);
   });
 
   function proceed() {
     if (!sourceshown || !popupshown || proceeded) {
       return;
     }
@@ -179,23 +180,23 @@ function secondSearch() {
   append("-0")
 }
 
 function thirdSearch() {
   let sourceshown = false;
   let popupshown = false;
   let proceeded = false;
 
-  window.addEventListener("Debugger:SourceShown", function _onEvent1(aEvent) {
-    window.removeEventListener(aEvent.type, _onEvent1);
+  gDebugger.addEventListener("Debugger:SourceShown", function _onEvent1(aEvent) {
+    gDebugger.removeEventListener(aEvent.type, _onEvent1);
     sourceshown = true;
     executeSoon(proceed);
   });
-  window.addEventListener("popupshown", function _onEvent2(aEvent) {
-    window.removeEventListener(aEvent.type, _onEvent2);
+  gDebugger.addEventListener("popupshown", function _onEvent2(aEvent) {
+    gDebugger.removeEventListener(aEvent.type, _onEvent2);
     popupshown = true;
     executeSoon(proceed);
   });
 
   function proceed() {
     if (!sourceshown || !popupshown || proceeded) {
       return;
     }
@@ -252,17 +253,17 @@ function thirdSearch() {
     } else {
       ok(false, "How did you get here?");
     }
   }
   backspace(1)
 }
 
 function goDown() {
-  window.addEventListener("Debugger:SourceShown", function _onEvent(aEvent) {
+  gDebugger.addEventListener("Debugger:SourceShown", function _onEvent(aEvent) {
     info("Current script url:\n" + gScripts.selectedValue + "\n");
     info("Debugger editor text:\n" + gEditor.getText() + "\n");
 
     is(gFilteredSources.totalItems, 3,
       "The filtered sources view should have 3 items available.");
     is(gFilteredSources.visibleItems.length, 3,
       "The filtered sources view should have 3 items visible.");
 
@@ -270,37 +271,37 @@ function goDown() {
        gDebugger.SourceUtils.trimUrlLength(gScripts.selectedValue),
       "The correct item should be selected in the filtered sources view");
     is(gFilteredSources.selectedLabel,
        gDebugger.SourceUtils.trimUrlLength(gScripts.selectedLabel),
       "The correct item should be selected in the filtered sources view");
 
     let url = gScripts.selectedValue;
     if (url.indexOf("test-editor-mode") != -1) {
-      window.removeEventListener(aEvent.type, _onEvent);
+      gDebugger.removeEventListener(aEvent.type, _onEvent);
 
       executeSoon(function() {
         info("Editor caret position: " + gEditor.getCaretPosition().toSource() + "\n");
         ok(gEditor.getCaretPosition().line == 0 &&
            gEditor.getCaretPosition().col == 0,
           "The editor didn't jump to the correct line.");
         is(gScripts.visibleItems.length, 3,
           "Not all the correct scripts are shown after the search.");
 
         goDownAgain();
       });
     } else {
       ok(false, "How did you get here?");
     }
   });
-  EventUtils.sendKey("DOWN");
+  EventUtils.sendKey("DOWN", gDebugger);
 }
 
 function goDownAgain() {
-  window.addEventListener("Debugger:SourceShown", function _onEvent(aEvent) {
+  gDebugger.addEventListener("Debugger:SourceShown", function _onEvent(aEvent) {
     info("Current script url:\n" + gScripts.selectedValue + "\n");
     info("Debugger editor text:\n" + gEditor.getText() + "\n");
 
     is(gFilteredSources.totalItems, 3,
       "The filtered sources view should have 3 items available.");
     is(gFilteredSources.visibleItems.length, 3,
       "The filtered sources view should have 3 items visible.");
 
@@ -308,37 +309,37 @@ function goDownAgain() {
        gDebugger.SourceUtils.trimUrlLength(gScripts.selectedValue),
       "The correct item should be selected in the filtered sources view");
     is(gFilteredSources.selectedLabel,
        gDebugger.SourceUtils.trimUrlLength(gScripts.selectedLabel),
       "The correct item should be selected in the filtered sources view");
 
     let url = gScripts.selectedValue;
     if (url.indexOf("test-script-switching-01.js") != -1) {
-      window.removeEventListener(aEvent.type, _onEvent);
+      gDebugger.removeEventListener(aEvent.type, _onEvent);
 
       executeSoon(function() {
         info("Editor caret position: " + gEditor.getCaretPosition().toSource() + "\n");
         ok(gEditor.getCaretPosition().line == 0 &&
            gEditor.getCaretPosition().col == 0,
           "The editor didn't jump to the correct line.");
         is(gScripts.visibleItems.length, 3,
           "Not all the correct scripts are shown after the search.");
 
         goDownAndWrap();
       });
     } else {
       ok(false, "How did you get here?");
     }
   });
-  EventUtils.synthesizeKey("g", { metaKey: true });
+  EventUtils.synthesizeKey("g", { metaKey: true }, gDebugger);
 }
 
 function goDownAndWrap() {
-  window.addEventListener("Debugger:SourceShown", function _onEvent(aEvent) {
+  gDebugger.addEventListener("Debugger:SourceShown", function _onEvent(aEvent) {
     info("Current script url:\n" + gScripts.selectedValue + "\n");
     info("Debugger editor text:\n" + gEditor.getText() + "\n");
 
     is(gFilteredSources.totalItems, 3,
       "The filtered sources view should have 3 items available.");
     is(gFilteredSources.visibleItems.length, 3,
       "The filtered sources view should have 3 items visible.");
 
@@ -346,37 +347,37 @@ function goDownAndWrap() {
        gDebugger.SourceUtils.trimUrlLength(gScripts.selectedValue),
       "The correct item should be selected in the filtered sources view");
     is(gFilteredSources.selectedLabel,
        gDebugger.SourceUtils.trimUrlLength(gScripts.selectedLabel),
       "The correct item should be selected in the filtered sources view");
 
     let url = gScripts.selectedValue;
     if (url.indexOf("update-editor-mode.html") != -1) {
-      window.removeEventListener(aEvent.type, _onEvent);
+      gDebugger.removeEventListener(aEvent.type, _onEvent);
 
       executeSoon(function() {
         info("Editor caret position: " + gEditor.getCaretPosition().toSource() + "\n");
         ok(gEditor.getCaretPosition().line == 0 &&
            gEditor.getCaretPosition().col == 0,
           "The editor didn't jump to the correct line.");
         is(gScripts.visibleItems.length, 3,
           "Not all the correct scripts are shown after the search.");
 
         goUpAndWrap();
       });
     } else {
       ok(false, "How did you get here?");
     }
   });
-  EventUtils.synthesizeKey("n", { ctrlKey: true });
+  EventUtils.synthesizeKey("n", { ctrlKey: true }, gDebugger);
 }
 
 function goUpAndWrap() {
-  window.addEventListener("Debugger:SourceShown", function _onEvent(aEvent) {
+  gDebugger.addEventListener("Debugger:SourceShown", function _onEvent(aEvent) {
     info("Current script url:\n" + gScripts.selectedValue + "\n");
     info("Debugger editor text:\n" + gEditor.getText() + "\n");
 
     is(gFilteredSources.totalItems, 3,
       "The filtered sources view should have 3 items available.");
     is(gFilteredSources.visibleItems.length, 3,
       "The filtered sources view should have 3 items visible.");
 
@@ -384,37 +385,37 @@ function goUpAndWrap() {
        gDebugger.SourceUtils.trimUrlLength(gScripts.selectedValue),
       "The correct item should be selected in the filtered sources view");
     is(gFilteredSources.selectedLabel,
        gDebugger.SourceUtils.trimUrlLength(gScripts.selectedLabel),
       "The correct item should be selected in the filtered sources view");
 
     let url = gScripts.selectedValue;
     if (url.indexOf("test-script-switching-01.js") != -1) {
-      window.removeEventListener(aEvent.type, _onEvent);
+      gDebugger.removeEventListener(aEvent.type, _onEvent);
 
       executeSoon(function() {
         info("Editor caret position: " + gEditor.getCaretPosition().toSource() + "\n");
         ok(gEditor.getCaretPosition().line == 0 &&
            gEditor.getCaretPosition().col == 0,
           "The editor didn't jump to the correct line.");
         is(gScripts.visibleItems.length, 3,
           "Not all the correct scripts are shown after the search.");
 
         clickAndSwitch();
       });
     } else {
       ok(false, "How did you get here?");
     }
   });
-  EventUtils.sendKey("UP");
+  EventUtils.sendKey("UP", gDebugger);
 }
 
 function clickAndSwitch() {
-  window.addEventListener("Debugger:SourceShown", function _onEvent(aEvent) {
+  gDebugger.addEventListener("Debugger:SourceShown", function _onEvent(aEvent) {
     info("Current script url:\n" + gScripts.selectedValue + "\n");
     info("Debugger editor text:\n" + gEditor.getText() + "\n");
 
     is(gFilteredSources.totalItems, 3,
       "The filtered sources view should have 3 items available.");
     is(gFilteredSources.visibleItems.length, 3,
       "The filtered sources view should have 3 items visible.");
 
@@ -422,17 +423,17 @@ function clickAndSwitch() {
        gDebugger.SourceUtils.trimUrlLength(gScripts.selectedValue),
       "The correct item should be selected in the filtered sources view");
     is(gFilteredSources.selectedLabel,
        gDebugger.SourceUtils.trimUrlLength(gScripts.selectedLabel),
       "The correct item should be selected in the filtered sources view");
 
     let url = gScripts.selectedValue;
     if (url.indexOf("update-editor-mode.html") != -1) {
-      window.removeEventListener(aEvent.type, _onEvent);
+      gDebugger.removeEventListener(aEvent.type, _onEvent);
 
       executeSoon(function() {
         info("Editor caret position: " + gEditor.getCaretPosition().toSource() + "\n");
         ok(gEditor.getCaretPosition().line == 0 &&
            gEditor.getCaretPosition().col == 0,
           "The editor didn't jump to the correct line.");
         is(gScripts.visibleItems.length, 3,
           "Not all the correct scripts are shown after the search.");
@@ -442,17 +443,17 @@ function clickAndSwitch() {
     } else {
       ok(false, "How did you get here?");
     }
   });
   EventUtils.sendMouseEvent({ type: "click" }, gFilteredSources.visibleItems[0].target);
 }
 
 function clickAndSwitchAgain() {
-  window.addEventListener("Debugger:SourceShown", function _onEvent(aEvent) {
+  gDebugger.addEventListener("Debugger:SourceShown", function _onEvent(aEvent) {
     info("Current script url:\n" + gScripts.selectedValue + "\n");
     info("Debugger editor text:\n" + gEditor.getText() + "\n");
 
     is(gFilteredSources.totalItems, 3,
       "The filtered sources view should have 3 items available.");
     is(gFilteredSources.visibleItems.length, 3,
       "The filtered sources view should have 3 items visible.");
 
@@ -460,17 +461,17 @@ function clickAndSwitchAgain() {
        gDebugger.SourceUtils.trimUrlLength(gScripts.selectedValue),
       "The correct item should be selected in the filtered sources view");
     is(gFilteredSources.selectedLabel,
        gDebugger.SourceUtils.trimUrlLength(gScripts.selectedLabel),
       "The correct item should be selected in the filtered sources view");
 
     let url = gScripts.selectedValue;
     if (url.indexOf("test-script-switching-01.js") != -1) {
-      window.removeEventListener(aEvent.type, _onEvent);
+      gDebugger.removeEventListener(aEvent.type, _onEvent);
 
       executeSoon(function() {
         info("Editor caret position: " + gEditor.getCaretPosition().toSource() + "\n");
         ok(gEditor.getCaretPosition().line == 0 &&
            gEditor.getCaretPosition().col == 0,
           "The editor didn't jump to the correct line.");
         is(gScripts.visibleItems.length, 3,
           "Not all the correct scripts are shown after the search.");
@@ -480,18 +481,18 @@ function clickAndSwitchAgain() {
     } else {
       ok(false, "How did you get here?");
     }
   });
   EventUtils.sendMouseEvent({ type: "click" }, gFilteredSources.visibleItems[2].target);
 }
 
 function switchFocusWithEscape() {
-  window.addEventListener("popuphidden", function _onEvent(aEvent) {
-    window.removeEventListener(aEvent.type, _onEvent);
+  gDebugger.addEventListener("popuphidden", function _onEvent(aEvent) {
+    gDebugger.removeEventListener(aEvent.type, _onEvent);
 
     info("Current script url:\n" + gScripts.selectedValue + "\n");
     info("Debugger editor text:\n" + gEditor.getText() + "\n");
 
     let url = gScripts.selectedValue;
     if (url.indexOf("test-script-switching-01.js") != -1) {
 
       executeSoon(function() {
@@ -503,22 +504,22 @@ function switchFocusWithEscape() {
           "Not all the correct scripts are shown after the search.");
 
         focusAgainAfterEscape();
       });
     } else {
       ok(false, "How did you get here?");
     }
   });
-  EventUtils.sendKey("ESCAPE");
+  EventUtils.sendKey("ESCAPE", gDebugger);
 }
 
 function focusAgainAfterEscape() {
-  window.addEventListener("popupshown", function _onEvent(aEvent) {
-    window.removeEventListener(aEvent.type, _onEvent);
+  gDebugger.addEventListener("popupshown", function _onEvent(aEvent) {
+    gDebugger.removeEventListener(aEvent.type, _onEvent);
 
     info("Current script url:\n" + gScripts.selectedValue + "\n");
     info("Debugger editor text:\n" + gEditor.getText() + "\n");
 
     let url = gScripts.selectedValue;
     if (url.indexOf("test-script-switching-01.js") != -1) {
 
       executeSoon(function() {
@@ -534,18 +535,18 @@ function focusAgainAfterEscape() {
     } else {
       ok(false, "How did you get here?");
     }
   });
   append("0");
 }
 
 function switchFocusWithReturn() {
-  window.addEventListener("popuphidden", function _onEvent(aEvent) {
-    window.removeEventListener(aEvent.type, _onEvent);
+  gDebugger.addEventListener("popuphidden", function _onEvent(aEvent) {
+    gDebugger.removeEventListener(aEvent.type, _onEvent);
 
     info("Current script url:\n" + gScripts.selectedValue + "\n");
     info("Debugger editor text:\n" + gEditor.getText() + "\n");
 
     let url = gScripts.selectedValue;
     if (url.indexOf("test-script-switching-01.js") != -1) {
 
       executeSoon(function() {
@@ -557,40 +558,40 @@ function switchFocusWithReturn() {
           "Not all the correct scripts are shown after the search.");
 
         closeDebuggerAndFinish();
       });
     } else {
       ok(false, "How did you get here?");
     }
   });
-  EventUtils.sendKey("RETURN");
+  EventUtils.sendKey("RETURN", gDebugger);
 }
 
 function clear() {
   gSearchBox.focus();
   gSearchBox.value = "";
 }
 
 function write(text) {
   clear();
   append(text);
 }
 
 function backspace(times) {
   for (let i = 0; i < times; i++) {
-    EventUtils.sendKey("BACK_SPACE")
+    EventUtils.sendKey("BACK_SPACE", gDebugger);
   }
 }
 
 function append(text) {
   gSearchBox.focus();
 
   for (let i = 0; i < text.length; i++) {
-    EventUtils.sendChar(text[i]);
+    EventUtils.sendChar(text[i], gDebugger);
   }
   info("Editor caret position: " + gEditor.getCaretPosition().toSource() + "\n");
 }
 
 registerCleanupFunction(function() {
   removeTab(gTab);
   gPane = null;
   gTab = null;
--- a/browser/devtools/debugger/test/browser_dbg_stack-05.js
+++ b/browser/devtools/debugger/test/browser_dbg_stack-05.js
@@ -19,33 +19,33 @@ function test() {
   let framesAdded = false;
 
   debug_tab_pane(TAB_URL, function(aTab, aDebuggee, aPane) {
     gTab = aTab;
     gDebuggee = aDebuggee;
     gPane = aPane;
     gDebugger = gPane.panelWin;
 
+    gDebugger.addEventListener("Debugger:SourceShown", function _onEvent(aEvent) {
+      let url = aEvent.detail.url;
+      if (url.indexOf("-02.js") != -1) {
+        scriptShown = true;
+        gDebugger.removeEventListener(aEvent.type, _onEvent);
+        runTest();
+      }
+    });
+
     gDebugger.DebuggerController.activeThread.addOneTimeListener("framesadded", function() {
       framesAdded = true;
       runTest();
     });
 
     gDebuggee.firstCall();
   });
 
-  window.addEventListener("Debugger:SourceShown", function _onEvent(aEvent) {
-    let url = aEvent.detail.url;
-    if (url.indexOf("-02.js") != -1) {
-      scriptShown = true;
-      window.removeEventListener(aEvent.type, _onEvent);
-      runTest();
-    }
-  });
-
   function runTest()
   {
     if (scriptShown && framesAdded) {
       Services.tm.currentThread.dispatch({ run: testRecurse }, 0);
     }
   }
 }
 
--- a/browser/devtools/debugger/test/browser_dbg_update-editor-mode.js
+++ b/browser/devtools/debugger/test/browser_dbg_update-editor-mode.js
@@ -28,37 +28,37 @@ function test()
   debug_tab_pane(TAB_URL, function(aTab, aDebuggee, aPane) {
     gTab = aTab;
     gDebuggee = aDebuggee;
     gPane = aPane;
     gDebugger = gPane.panelWin;
     gScripts = gDebugger.DebuggerView.Sources._container;
     resumed = true;
 
+    gDebugger.addEventListener("Debugger:SourceShown", onScriptShown);
+
     gDebugger.DebuggerController.activeThread.addOneTimeListener("framesadded", function() {
       framesAdded = true;
       executeSoon(startTest);
     });
 
     executeSoon(function() {
       gDebuggee.firstCall();
     });
   });
 
   function onScriptShown(aEvent) {
     scriptShown = aEvent.detail.url.indexOf("test-editor-mode") != -1;
     executeSoon(startTest);
   }
 
-  window.addEventListener("Debugger:SourceShown", onScriptShown);
-
   function startTest()
   {
     if (scriptShown && framesAdded && resumed && !testStarted) {
-      window.removeEventListener("Debugger:SourceShown", onScriptShown);
+      gDebugger.removeEventListener("Debugger:SourceShown", onScriptShown);
       testStarted = true;
       Services.tm.currentThread.dispatch({ run: testScriptsDisplay }, 0);
     }
   }
 }
 
 function testScriptsDisplay() {
   is(gDebugger.DebuggerController.activeThread.state, "paused",
@@ -68,20 +68,20 @@ function testScriptsDisplay() {
     "Found the expected number of scripts.");
 
   is(gDebugger.editor.getMode(), SourceEditor.MODES.TEXT,
      "Found the expected editor mode.");
 
   ok(gDebugger.editor.getText().search(/debugger/) != -1,
     "The correct script was loaded initially.");
 
-  window.addEventListener("Debugger:SourceShown", function _onEvent(aEvent) {
+  gDebugger.addEventListener("Debugger:SourceShown", function _onEvent(aEvent) {
     let url = aEvent.detail.url;
     if (url.indexOf("switching-01.js") != -1) {
-      window.removeEventListener(aEvent.type, _onEvent);
+      gDebugger.removeEventListener(aEvent.type, _onEvent);
       testSwitchPaused1();
     }
   });
 
   let url = gDebuggee.document.querySelector("script").src;
   gDebugger.DebuggerView.Sources.selectedValue = url;
 }
 
@@ -97,20 +97,20 @@ function testSwitchPaused1()
     "The second script is no longer displayed.");
 
   ok(gDebugger.editor.getText().search(/firstCall/) != -1,
     "The first script is displayed.");
 
   is(gDebugger.editor.getMode(), SourceEditor.MODES.JAVASCRIPT,
      "Found the expected editor mode.");
 
-  window.addEventListener("Debugger:SourceShown", function _onEvent(aEvent) {
+  gDebugger.addEventListener("Debugger:SourceShown", function _onEvent(aEvent) {
     let url = aEvent.detail.url;
     if (url.indexOf("update-editor-mode") != -1) {
-      window.removeEventListener(aEvent.type, _onEvent);
+      gDebugger.removeEventListener(aEvent.type, _onEvent);
       testSwitchPaused2();
     }
   });
 
   let label = "browser_dbg_update-editor-mode.html";
   gDebugger.DebuggerView.Sources.selectedLabel = label;
 }
 
--- a/browser/devtools/framework/ToolDefinitions.jsm
+++ b/browser/devtools/framework/ToolDefinitions.jsm
@@ -144,16 +144,17 @@ let styleEditorDefinition = {
   }
 };
 
 let profilerDefinition = {
   id: "jsprofiler",
   killswitch: "devtools.profiler.enabled",
   url: "chrome://browser/content/profiler.xul",
   label: l10n("profiler.label", profilerStrings),
+  icon: "chrome://browser/skin/devtools/tool-profiler.png",
   tooltip: l10n("profiler.tooltip", profilerStrings),
 
   isTargetSupported: function (target) {
     return !target.isRemote;
   },
 
   build: function (frame, target) {
     let panel = new ProfilerPanel(frame, target);
--- a/browser/devtools/styleeditor/StyleEditorChrome.jsm
+++ b/browser/devtools/styleeditor/StyleEditorChrome.jsm
@@ -378,18 +378,20 @@ StyleEditorChrome.prototype = {
               if (newSheet != sheet) {
                 self._window.setTimeout(self.selectStyleSheet.bind(self, newSheet, newLine, newCol), 0);
               }
             }
           });
         } else {
           // If a line or column was specified we move the caret appropriately.
           aEditor.sourceEditor.setCaretPosition(line - 1, col - 1);
-          self._styleSheetToSelect = null;
+          this._styleSheetToSelect = null;
         }
+      } else {
+        this._styleSheetToSelect = null;
       }
 
       this._view.activeSummary = summary;
       this.selectedStyleSheetIndex = aEditor.styleSheetIndex;
     }.bind(this);
 
     if (!this.editors.length) {
       // We are in the main initialization phase so we wait for the editor
--- a/browser/installer/package-manifest.in
+++ b/browser/installer/package-manifest.in
@@ -468,19 +468,16 @@
 #ifdef MOZ_SERVICES_AITC
 @BINPATH@/components/AitcComponents.manifest
 @BINPATH@/components/Aitc.js
 #endif
 #ifdef MOZ_SERVICES_HEALTHREPORT
 @BINPATH@/components/HealthReportComponents.manifest
 @BINPATH@/components/HealthReportService.js
 #endif
-#ifdef MOZ_SERVICES_NOTIFICATIONS
-@BINPATH@/components/NotificationsComponents.manifest
-#endif
 #ifdef MOZ_SERVICES_SYNC
 @BINPATH@/components/SyncComponents.manifest
 @BINPATH@/components/Weave.js
 #endif
 @BINPATH@/components/TelemetryPing.js
 @BINPATH@/components/TelemetryPing.manifest
 @BINPATH@/components/messageWakeupService.js
 @BINPATH@/components/messageWakeupService.manifest
@@ -591,19 +588,16 @@
 @BINPATH@/defaults/pref/services-aitc.js
 #endif
 #ifdef MOZ_SERVICES_NOTIFICATIONS
 @BINPATH@/defaults/pref/services-notifications.js
 #endif
 #ifdef MOZ_SERVICES_SYNC
 @BINPATH@/defaults/pref/services-sync.js
 #endif
-#ifdef MOZ_SERVICES_HEALTHREPORT
-@BINPATH@/defaults/pref/healthreport-prefs.js
-#endif
 
 ; [Layout Engine Resources]
 ; Style Sheets, Graphics and other Resources used by the layout engine. 
 @BINPATH@/res/EditorOverride.css
 @BINPATH@/res/contenteditable.css
 @BINPATH@/res/designmode.css
 @BINPATH@/res/TopLevelImageDocument.css
 @BINPATH@/res/TopLevelVideoDocument.css
--- a/browser/installer/removed-files.in
+++ b/browser/installer/removed-files.in
@@ -895,16 +895,17 @@ xpicleanup@BIN_SUFFIX@
   components/FeedConverter.js
   components/FeedProcessor.js
   components/FeedWriter.js
   components/fuelApplication.js
   components/GPSDGeolocationProvider.js
   components/interfaces.manifest
   components/jsconsole-clhandler.js
   components/NetworkGeolocationProvider.js
+  components/NotificationsComponents.manifest
   components/nsBadCertHandler.js
   components/nsBlocklistService.js
   components/nsBrowserContentHandler.js
   components/nsBrowserGlue.js
   components/nsContentDispatchChooser.js
   components/nsContentPrefService.js
   components/nsDefaultCLH.js
   components/nsDownloadManagerUI.js
@@ -1051,16 +1052,17 @@ xpicleanup@BIN_SUFFIX@
   modules/Services.jsm
   modules/services-common/async.js
   modules/services-common/log4moz.js
   modules/services-common/observers.js
   modules/services-common/preferences.js
   modules/services-common/rest.js
   modules/services-common/stringbundle.js
   modules/services-common/utils.js
+  modules/services-notifications/service.js
   modules/services-sync/auth.js
   modules/services-sync/base_records/collection.js
   modules/services-sync/base_records/crypto.js
   modules/services-sync/base_records/keys.js
   modules/services-sync/base_records/wbo.js
   modules/services-sync/constants.js
   modules/services-sync/engines/bookmarks.js
   modules/services-sync/engines/clients.js
index 83842f236ef5ede55a268e67077532fc1764746f..8fa9c932bd9f26288f736f5d9820d8ff76f88569
GIT binary patch
literal 1383
zc$@)e1(^DYP)<h;3K|Lk000e1NJLTq000mG000mO1^@s6AM^iV000CeX+uL$Nkc;*
zP;zf(X>4Tx05}naRo`#hR1`jmZ&IWdKOk5~hl<6oRa0BJ8yc;~21%2p?MfD<>DVeH
z<T^KrsT&8|>9(p*dx19w`~g7O0}n_%Aq@s%d)fBDv`JHkDym6Hd+5XuAtvnwRpGmK
zVkc9?T=n|PIo~<wJLg{8L_J?=wVD}Kh?c9aozEndlcyGxo=u9<v(!ri)T`-EEs@L3
z5-!0N_s;9#9f}Cc?UC;OPWB_edW+oAi6T$HZWSGU8TbrQ%+zbPOBBBc`}k?M2Hf);
z@Y6N~0;>X-eVh__(Z?q}P9Z-Dj?gOW6|D%o20XmjW-qs4UjrD(li^iv8@eK9k+ZFm
zVRFymFOPAzG5-%Pn|1W;U4vNroTa&AxDScmEA~{ri9gr1^c?U@uwSpaNnw8l_>cP1
zd;)kMQS_;jeRSUEM_*s96y65j1$)tOrwdK{YIQMt92l|D^(E_=$Rjw{b!QT@q!)ni
zR`|5oW9X5n$Wv+HVc@|^eX5yXnsHX<gx$-tTA9oOBadXir_JPm2Y^4ct-PoO&C)tI
zGolvqOIK@duBk!Vu9{g<3;i;gJ6?~-DQ&xz!jvD&4!U-s8Os(*#?k2}f30SEXA#=i
z1-qUX+K`{!((H5w7<t$~ygD!D1{~X6)KX%$qrgY#L_{M_7A<1csY*MfP@XcB#Jxr~
zJS8&7goVS)VKE|4(h_Xlc{z{c$ApZs7riZ_QKdV_uW-M~u~<J-*#Z0?VzcZp8)p-w
zus7J7><CN2I>8PF3UX~a6)MwxDE0HaPjyrlI!;jX{6Kvuh*8ej?;85ekN$?5uuCiS
zBTvvVG+XTxAO{m@bvM#Jr)z6J><&E22D|vq?Y?Vkbo_DijopiF$2PET#<s%v*srlI
z{B2SKJ79W>mZ8e<cESmGBON_l0n;T7>u=y$(ArYkv7@Ex`GL?QCc!_*KFrd&;n1r7
zqW-CFs9&fT)ZaU5gc&=gBz-D<EBz>aCw(vdOp0__x+47~U6sC(E(JNe@4cTT*n6*E
zVH4eoU1-&7pEV~_PRe`a7v+@vy!^5}8?Y3)UmlaE<h}6h3HHql{T;m+bPBU-O|^S1
z@dOw&4<!bj2G_<^#e}PL7FpY$lcrKO$i~?8Bd2y;oaL5^csibnCrF9!i%-PI;xhub
zp1k;8_$IKX1NHus6EHeD;B72SCCD@4ojP$=Mf3`Eo6yZ&eg@wTqDiZE);7u&SJ|(s
zuPF(9%D6IJ)klXF%`_Fy<tR3HxV^%Qqa?nAB97=m-uu2qcHInZ?ps8M|H3=#R%lzO
z6MgLv^}ib0hVV{&<};#;2lcwW;^(7C<OY#bI<VjS9qCKr-E_Cnc!2j+&nHAXA2%BR
zt~VMxUn2h&(Pi^LSpac(Y#S>R00009a7bBm000XU000XU0RWnu7ytkO=}AOER5%f(
zQLzfbKn%TH3qrL{#X+20{KI~xyStNgP*FkK8k6Fcm!k@$2chrYdr6ZVF|(U;{FG$P
zom?Qr4j#|;NW@FzV$4j}Y?f?#UUv}U{;bbsS*~4C<U^sFdV;^(A6`VPq%t9gST*$^
zXLrEVi1_naI!!!%`vPnM=ZuI(z{V37&>3<t8~D{~B+V-@ah<)|{R16<Gr{Ts%9#Qr
zBeYF(Ktqg^G}$2+kI<0!D>1Y~@zu-@Ob72bU%aQe^T`(ff>=Fx@7_ERM5~1>SSI;%
pDor`vg2NOD<d|=~gE6_C_XB1vk}(x*QltO?002ovPDHLkV1lF4lNA5}
index 9a940bc1e4e50a63d1522fc5604cb2cf44b05e82..1c12355138bd03b6ed7529809e61b601530b624f
GIT binary patch
literal 1415
zc$@);1$g?2P)<h;3K|Lk000e1NJLTq000mG000mO1^@s6AM^iV000CeX+uL$Nkc;*
zP;zf(X>4Tx05}naRo`#hR1`jmZ&IWdKOk5~hl<6oRa0BJ8yc;~21%2p?MfD<>DVeH
z<T^KrsT&8|>9(p*dx19w`~g7O0}n_%Aq@s%d)fBDv`JHkDym6Hd+5XuAtvnwRpGmK
zVkc9?T=n|PIo~<wJLg{8L_J?=wVD}Kh?c9aozEndlcyGxo=u9<v(!ri)T`-EEs@L3
z5-!0N_s;9#9f}Cc?UC;OPWB_edW+oAi6T$HZWSGU8TbrQ%+zbPOBBBc`}k?M2Hf);
z@Y6N~0;>X-eVh__(Z?q}P9Z-Dj?gOW6|D%o20XmjW-qs4UjrD(li^iv8@eK9k+ZFm
zVRFymFOPAzG5-%Pn|1W;U4vNroTa&AxDScmEA~{ri9gr1^c?U@uwSpaNnw8l_>cP1
zd;)kMQS_;jeRSUEM_*s96y65j1$)tOrwdK{YIQMt92l|D^(E_=$Rjw{b!QT@q!)ni
zR`|5oW9X5n$Wv+HVc@|^eX5yXnsHX<gx$-tTA9oOBadXir_JPm2Y^4ct-PoO&C)tI
zGolvqOIK@duBk!Vu9{g<3;i;gJ6?~-DQ&xz!jvD&4!U-s8Os(*#?k2}f30SEXA#=i
z1-qUX+K`{!((H5w7<t$~ygD!D1{~X6)KX%$qrgY#L_{M_7A<1csY*MfP@XcB#Jxr~
zJS8&7goVS)VKE|4(h_Xlc{z{c$ApZs7riZ_QKdV_uW-M~u~<J-*#Z0?VzcZp8)p-w
zus7J7><CN2I>8PF3UX~a6)MwxDE0HaPjyrlI!;jX{6Kvuh*8ej?;85ekN$?5uuCiS
zBTvvVG+XTxAO{m@bvM#Jr)z6J><&E22D|vq?Y?Vkbo_DijopiF$2PET#<s%v*srlI
z{B2SKJ79W>mZ8e<cESmGBON_l0n;T7>u=y$(ArYkv7@Ex`GL?QCc!_*KFrd&;n1r7
zqW-CFs9&fT)ZaU5gc&=gBz-D<EBz>aCw(vdOp0__x+47~U6sC(E(JNe@4cTT*n6*E
zVH4eoU1-&7pEV~_PRe`a7v+@vy!^5}8?Y3)UmlaE<h}6h3HHql{T;m+bPBU-O|^S1
z@dOw&4<!bj2G_<^#e}PL7FpY$lcrKO$i~?8Bd2y;oaL5^csibnCrF9!i%-PI;xhub
zp1k;8_$IKX1NHus6EHeD;B72SCCD@4ojP$=Mf3`Eo6yZ&eg@wTqDiZE);7u&SJ|(s
zuPF(9%D6IJ)klXF%`_Fy<tR3HxV^%Qqa?nAB97=m-uu2qcHInZ?ps8M|H3=#R%lzO
z6MgLv^}ib0hVV{&<};#;2lcwW;^(7C<OY#bI<VjS9qCKr-E_Cnc!2j+&nHAXA2%BR
zt~VMxUn2h&(Pi^LSpac(Y#S>R00009a7bBm000XU000XU0RWnu7ytkP2}wjjR5%f(
zQA-YjKn$G%YT|E&EQ~9+-j5S<1rK22N?oV{jq<52PH{SkCWa<u+Sm8mp#z9$`dH9M
z%-Y@!BKHQjN4*EYxr_NRB3#F#aMj_w1M%Wfp3^j4nmC?`bI$msxM%tOx#L*)d;)*O
zZqMp884VVMbs&(D=vbH4mU-bI+Kh)$EE>H7gvu2&sm%$eZM~gi$J7o8S4D?MjZJNH
z#qk@Ij>ruO?<OmbYJh;MC><=`PWCyf0etxd#>v}X@Hu*k?*>zuY){U4dJRF4*w~hg
zQ8#EBywFo-UG)=GVHB=8WzGkt%{WhP0FtSRk#70JPAoa(!cYwa{O9y{@Q=3Tz5x9H
Vj0(v1c&q>b002ovPDHLkV1mI<q`UwC
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..8f30f12a9d52bd62ee7b10180dd66fd8f45e72b7
GIT binary patch
literal 1834
zc$@($2i5qAP)<h;3K|Lk000e1NJLTq000mG000mO1^@s6AM^iV000CeX+uL$Nkc;*
zP;zf(X>4Tx05}naRo`#hR1`jmZ&IWdKOk5~hl<6oRa0BJ8yc;~21%2p?MfD<>DVeH
z<T^KrsT&8|>9(p*dx19w`~g7O0}n_%Aq@s%d)fBDv`JHkDym6Hd+5XuAtvnwRpGmK
zVkc9?T=n|PIo~<wJLg{8L_J?=wVD}Kh?c9aozEndlcyGxo=u9<v(!ri)T`-EEs@L3
z5-!0N_s;9#9f}Cc?UC;OPWB_edW+oAi6T$HZWSGU8TbrQ%+zbPOBBBc`}k?M2Hf);
z@Y6N~0;>X-eVh__(Z?q}P9Z-Dj?gOW6|D%o20XmjW-qs4UjrD(li^iv8@eK9k+ZFm
zVRFymFOPAzG5-%Pn|1W;U4vNroTa&AxDScmEA~{ri9gr1^c?U@uwSpaNnw8l_>cP1
zd;)kMQS_;jeRSUEM_*s96y65j1$)tOrwdK{YIQMt92l|D^(E_=$Rjw{b!QT@q!)ni
zR`|5oW9X5n$Wv+HVc@|^eX5yXnsHX<gx$-tTA9oOBadXir_JPm2Y^4ct-PoO&C)tI
zGolvqOIK@duBk!Vu9{g<3;i;gJ6?~-DQ&xz!jvD&4!U-s8Os(*#?k2}f30SEXA#=i
z1-qUX+K`{!((H5w7<t$~ygD!D1{~X6)KX%$qrgY#L_{M_7A<1csY*MfP@XcB#Jxr~
zJS8&7goVS)VKE|4(h_Xlc{z{c$ApZs7riZ_QKdV_uW-M~u~<J-*#Z0?VzcZp8)p-w
zus7J7><CN2I>8PF3UX~a6)MwxDE0HaPjyrlI!;jX{6Kvuh*8ej?;85ekN$?5uuCiS
zBTvvVG+XTxAO{m@bvM#Jr)z6J><&E22D|vq?Y?Vkbo_DijopiF$2PET#<s%v*srlI
z{B2SKJ79W>mZ8e<cESmGBON_l0n;T7>u=y$(ArYkv7@Ex`GL?QCc!_*KFrd&;n1r7
zqW-CFs9&fT)ZaU5gc&=gBz-D<EBz>aCw(vdOp0__x+47~U6sC(E(JNe@4cTT*n6*E
zVH4eoU1-&7pEV~_PRe`a7v+@vy!^5}8?Y3)UmlaE<h}6h3HHql{T;m+bPBU-O|^S1
z@dOw&4<!bj2G_<^#e}PL7FpY$lcrKO$i~?8Bd2y;oaL5^csibnCrF9!i%-PI;xhub
zp1k;8_$IKX1NHus6EHeD;B72SCCD@4ojP$=Mf3`Eo6yZ&eg@wTqDiZE);7u&SJ|(s
zuPF(9%D6IJ)klXF%`_Fy<tR3HxV^%Qqa?nAB97=m-uu2qcHInZ?ps8M|H3=#R%lzO
z6MgLv^}ib0hVV{&<};#;2lcwW;^(7C<OY#bI<VjS9qCKr-E_Cnc!2j+&nHAXA2%BR
zt~VMxUn2h&(Pi^LSpac(Y#S>R00009a7bBm000XU000XU0RWnu7ytkQtVu*cR5%f(
zQ%h)5K@^>N`81{eYe^ScQ%c)_i!NNb5K_An!G&E^QAAt_6$QaA+(=h-VRh455fL;B
zrhcJ*T8pTl_<=OoLZo7=lKzsWO-)|j|0UzOPaA_<2kx7hbI(0<=gxgx2*Lhmxc?1a
zVQjtEv^%+Adl=)*j0*{zY1oLc+*hiqSh75ZmuH~pb{psX8iF>JlS*sBHO|@TO0O%j
zij@TNy4@;qeiy+m-Oyhoe$3pseEsSe5l<XDzN0R%^{B7H*G7B6_^B$d>o%<!WsnwP
zt!h}4ieW9yCKu0!!{NH*%E~^tIaxZB+dDiw+`N!p?j{a0o!2z2-pQUmvZ8kaOf6?+
zO3e$Cz9te4@JTTKQ&!vay4f2D1bmrn{tj_-s@eNjK(`)}YJ75PH-U}=BAD0U#2)w(
zA7il=!ihs%2#(R7DvUfuJto`lui2uRg?6mHa7wCaKWBm;lJmLn=WO;1`G`YY&ZHX5
zmCk8Ui$eALz`(#pfF4qYxn<3KlU1~LJ^lUFgprRp#D#r}Mx!1_AS34o6~ms&<<y;j
zf*m{*3T+@mpz~VfBMxyP{Fe4kBdN0T(pO#4!adTF3}f*MtFWIJi@v8Lqjah>6sw>c
z9@hgeIJmNHS>|iTe7|cj^6c@wLQyP9l53Y&H4ctG85+`ca~cO|WV9xXe8j<JwD|a^
zo22ZdhVJ8I<MGyDu;~&o7Yg?(olHd{ky~*hb#xrwUQ_Eo0CoslF*iGVzNw*p5LYOc
zIuRP~*u+Gru`zf~lK46hsfo%5RneG9%wG&PZhM4a%W+a6Rt<o=tE;<qF1dIzo7YfS
z>jPfmeNNHuFQn3EF7)-$bFaRV6IaT>QNxF>g9+W}ceBj}i7ny3L{Bl6vVog8j4~;W
YU#@V)?0xW-3IG5A07*qoM6N<$g6IHrzW@LL
--- a/browser/themes/gnomestripe/devtools/toolbox.css
+++ b/browser/themes/gnomestripe/devtools/toolbox.css
@@ -62,17 +62,17 @@
   padding: 0 8px;
   margin: 0;
   width: 16px;
 }
 
 .command-button:hover {
   background-color: hsla(206,37%,4%,.2);
 }
-.command-button:hover:active {
+.command-button:hover:active, .command-button[checked=true]:not(:hover) {
   background-color: hsla(206,37%,4%,.4);
 }
 
 #command-button-responsive {
   list-style-image: url("chrome://browser/skin/devtools/command-responsivemode.png");
   -moz-image-region: rect(0px, 16px, 16px, 0px);
 }
 #command-button-responsive:hover {
--- a/browser/themes/gnomestripe/jar.mn
+++ b/browser/themes/gnomestripe/jar.mn
@@ -177,16 +177,17 @@ browser.jar:
   skin/classic/browser/devtools/dock-side.png             (devtools/dock-side.png)
   skin/classic/browser/devtools/floating-scrollbars.css   (devtools/floating-scrollbars.css)
   skin/classic/browser/devtools/inspector.css             (devtools/inspector.css)
   skin/classic/browser/devtools/toolbox.css               (devtools/toolbox.css)
   skin/classic/browser/devtools/tool-webconsole.png       (devtools/tool-webconsole.png)
   skin/classic/browser/devtools/tool-debugger.png         (devtools/tool-debugger.png)
   skin/classic/browser/devtools/tool-inspector.png        (devtools/tool-inspector.png)
   skin/classic/browser/devtools/tool-styleeditor.png      (devtools/tool-styleeditor.png)
+  skin/classic/browser/devtools/tool-profiler.png         (devtools/tool-profiler.png)
   skin/classic/browser/devtools/close.png                 (devtools/close.png)
   skin/classic/browser/devtools/undock.png                (devtools/undock.png)
 #ifdef MOZ_SERVICES_SYNC
   skin/classic/browser/sync-16-throbber.png
   skin/classic/browser/sync-16.png
   skin/classic/browser/sync-24-throbber.png
   skin/classic/browser/sync-32.png
   skin/classic/browser/sync-bg.png
index 83842f236ef5ede55a268e67077532fc1764746f..8fa9c932bd9f26288f736f5d9820d8ff76f88569
GIT binary patch
literal 1383
zc$@)e1(^DYP)<h;3K|Lk000e1NJLTq000mG000mO1^@s6AM^iV000CeX+uL$Nkc;*
zP;zf(X>4Tx05}naRo`#hR1`jmZ&IWdKOk5~hl<6oRa0BJ8yc;~21%2p?MfD<>DVeH
z<T^KrsT&8|>9(p*dx19w`~g7O0}n_%Aq@s%d)fBDv`JHkDym6Hd+5XuAtvnwRpGmK
zVkc9?T=n|PIo~<wJLg{8L_J?=wVD}Kh?c9aozEndlcyGxo=u9<v(!ri)T`-EEs@L3
z5-!0N_s;9#9f}Cc?UC;OPWB_edW+oAi6T$HZWSGU8TbrQ%+zbPOBBBc`}k?M2Hf);
z@Y6N~0;>X-eVh__(Z?q}P9Z-Dj?gOW6|D%o20XmjW-qs4UjrD(li^iv8@eK9k+ZFm
zVRFymFOPAzG5-%Pn|1W;U4vNroTa&AxDScmEA~{ri9gr1^c?U@uwSpaNnw8l_>cP1
zd;)kMQS_;jeRSUEM_*s96y65j1$)tOrwdK{YIQMt92l|D^(E_=$Rjw{b!QT@q!)ni
zR`|5oW9X5n$Wv+HVc@|^eX5yXnsHX<gx$-tTA9oOBadXir_JPm2Y^4ct-PoO&C)tI
zGolvqOIK@duBk!Vu9{g<3;i;gJ6?~-DQ&xz!jvD&4!U-s8Os(*#?k2}f30SEXA#=i
z1-qUX+K`{!((H5w7<t$~ygD!D1{~X6)KX%$qrgY#L_{M_7A<1csY*MfP@XcB#Jxr~
zJS8&7goVS)VKE|4(h_Xlc{z{c$ApZs7riZ_QKdV_uW-M~u~<J-*#Z0?VzcZp8)p-w
zus7J7><CN2I>8PF3UX~a6)MwxDE0HaPjyrlI!;jX{6Kvuh*8ej?;85ekN$?5uuCiS
zBTvvVG+XTxAO{m@bvM#Jr)z6J><&E22D|vq?Y?Vkbo_DijopiF$2PET#<s%v*srlI
z{B2SKJ79W>mZ8e<cESmGBON_l0n;T7>u=y$(ArYkv7@Ex`GL?QCc!_*KFrd&;n1r7
zqW-CFs9&fT)ZaU5gc&=gBz-D<EBz>aCw(vdOp0__x+47~U6sC(E(JNe@4cTT*n6*E
zVH4eoU1-&7pEV~_PRe`a7v+@vy!^5}8?Y3)UmlaE<h}6h3HHql{T;m+bPBU-O|^S1
z@dOw&4<!bj2G_<^#e}PL7FpY$lcrKO$i~?8Bd2y;oaL5^csibnCrF9!i%-PI;xhub
zp1k;8_$IKX1NHus6EHeD;B72SCCD@4ojP$=Mf3`Eo6yZ&eg@wTqDiZE);7u&SJ|(s
zuPF(9%D6IJ)klXF%`_Fy<tR3HxV^%Qqa?nAB97=m-uu2qcHInZ?ps8M|H3=#R%lzO
z6MgLv^}ib0hVV{&<};#;2lcwW;^(7C<OY#bI<VjS9qCKr-E_Cnc!2j+&nHAXA2%BR
zt~VMxUn2h&(Pi^LSpac(Y#S>R00009a7bBm000XU000XU0RWnu7ytkO=}AOER5%f(
zQLzfbKn%TH3qrL{#X+20{KI~xyStNgP*FkK8k6Fcm!k@$2chrYdr6ZVF|(U;{FG$P
zom?Qr4j#|;NW@FzV$4j}Y?f?#UUv}U{;bbsS*~4C<U^sFdV;^(A6`VPq%t9gST*$^
zXLrEVi1_naI!!!%`vPnM=ZuI(z{V37&>3<t8~D{~B+V-@ah<)|{R16<Gr{Ts%9#Qr
zBeYF(Ktqg^G}$2+kI<0!D>1Y~@zu-@Ob72bU%aQe^T`(ff>=Fx@7_ERM5~1>SSI;%
pDor`vg2NOD<d|=~gE6_C_XB1vk}(x*QltO?002ovPDHLkV1lF4lNA5}
index 9a940bc1e4e50a63d1522fc5604cb2cf44b05e82..1c12355138bd03b6ed7529809e61b601530b624f
GIT binary patch
literal 1415
zc$@);1$g?2P)<h;3K|Lk000e1NJLTq000mG000mO1^@s6AM^iV000CeX+uL$Nkc;*
zP;zf(X>4Tx05}naRo`#hR1`jmZ&IWdKOk5~hl<6oRa0BJ8yc;~21%2p?MfD<>DVeH
z<T^KrsT&8|>9(p*dx19w`~g7O0}n_%Aq@s%d)fBDv`JHkDym6Hd+5XuAtvnwRpGmK
zVkc9?T=n|PIo~<wJLg{8L_J?=wVD}Kh?c9aozEndlcyGxo=u9<v(!ri)T`-EEs@L3
z5-!0N_s;9#9f}Cc?UC;OPWB_edW+oAi6T$HZWSGU8TbrQ%+zbPOBBBc`}k?M2Hf);
z@Y6N~0;>X-eVh__(Z?q}P9Z-Dj?gOW6|D%o20XmjW-qs4UjrD(li^iv8@eK9k+ZFm
zVRFymFOPAzG5-%Pn|1W;U4vNroTa&AxDScmEA~{ri9gr1^c?U@uwSpaNnw8l_>cP1
zd;)kMQS_;jeRSUEM_*s96y65j1$)tOrwdK{YIQMt92l|D^(E_=$Rjw{b!QT@q!)ni
zR`|5oW9X5n$Wv+HVc@|^eX5yXnsHX<gx$-tTA9oOBadXir_JPm2Y^4ct-PoO&C)tI
zGolvqOIK@duBk!Vu9{g<3;i;gJ6?~-DQ&xz!jvD&4!U-s8Os(*#?k2}f30SEXA#=i
z1-qUX+K`{!((H5w7<t$~ygD!D1{~X6)KX%$qrgY#L_{M_7A<1csY*MfP@XcB#Jxr~
zJS8&7goVS)VKE|4(h_Xlc{z{c$ApZs7riZ_QKdV_uW-M~u~<J-*#Z0?VzcZp8)p-w
zus7J7><CN2I>8PF3UX~a6)MwxDE0HaPjyrlI!;jX{6Kvuh*8ej?;85ekN$?5uuCiS
zBTvvVG+XTxAO{m@bvM#Jr)z6J><&E22D|vq?Y?Vkbo_DijopiF$2PET#<s%v*srlI
z{B2SKJ79W>mZ8e<cESmGBON_l0n;T7>u=y$(ArYkv7@Ex`GL?QCc!_*KFrd&;n1r7
zqW-CFs9&fT)ZaU5gc&=gBz-D<EBz>aCw(vdOp0__x+47~U6sC(E(JNe@4cTT*n6*E
zVH4eoU1-&7pEV~_PRe`a7v+@vy!^5}8?Y3)UmlaE<h}6h3HHql{T;m+bPBU-O|^S1
z@dOw&4<!bj2G_<^#e}PL7FpY$lcrKO$i~?8Bd2y;oaL5^csibnCrF9!i%-PI;xhub
zp1k;8_$IKX1NHus6EHeD;B72SCCD@4ojP$=Mf3`Eo6yZ&eg@wTqDiZE);7u&SJ|(s
zuPF(9%D6IJ)klXF%`_Fy<tR3HxV^%Qqa?nAB97=m-uu2qcHInZ?ps8M|H3=#R%lzO
z6MgLv^}ib0hVV{&<};#;2lcwW;^(7C<OY#bI<VjS9qCKr-E_Cnc!2j+&nHAXA2%BR
zt~VMxUn2h&(Pi^LSpac(Y#S>R00009a7bBm000XU000XU0RWnu7ytkP2}wjjR5%f(
zQA-YjKn$G%YT|E&EQ~9+-j5S<1rK22N?oV{jq<52PH{SkCWa<u+Sm8mp#z9$`dH9M
z%-Y@!BKHQjN4*EYxr_NRB3#F#aMj_w1M%Wfp3^j4nmC?`bI$msxM%tOx#L*)d;)*O
zZqMp884VVMbs&(D=vbH4mU-bI+Kh)$EE>H7gvu2&sm%$eZM~gi$J7o8S4D?MjZJNH
z#qk@Ij>ruO?<OmbYJh;MC><=`PWCyf0etxd#>v}X@Hu*k?*>zuY){U4dJRF4*w~hg
zQ8#EBywFo-UG)=GVHB=8WzGkt%{WhP0FtSRk#70JPAoa(!cYwa{O9y{@Q=3Tz5x9H
Vj0(v1c&q>b002ovPDHLkV1mI<q`UwC
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..8f30f12a9d52bd62ee7b10180dd66fd8f45e72b7
GIT binary patch
literal 1834
zc$@($2i5qAP)<h;3K|Lk000e1NJLTq000mG000mO1^@s6AM^iV000CeX+uL$Nkc;*
zP;zf(X>4Tx05}naRo`#hR1`jmZ&IWdKOk5~hl<6oRa0BJ8yc;~21%2p?MfD<>DVeH
z<T^KrsT&8|>9(p*dx19w`~g7O0}n_%Aq@s%d)fBDv`JHkDym6Hd+5XuAtvnwRpGmK
zVkc9?T=n|PIo~<wJLg{8L_J?=wVD}Kh?c9aozEndlcyGxo=u9<v(!ri)T`-EEs@L3
z5-!0N_s;9#9f}Cc?UC;OPWB_edW+oAi6T$HZWSGU8TbrQ%+zbPOBBBc`}k?M2Hf);
z@Y6N~0;>X-eVh__(Z?q}P9Z-Dj?gOW6|D%o20XmjW-qs4UjrD(li^iv8@eK9k+ZFm
zVRFymFOPAzG5-%Pn|1W;U4vNroTa&AxDScmEA~{ri9gr1^c?U@uwSpaNnw8l_>cP1
zd;)kMQS_;jeRSUEM_*s96y65j1$)tOrwdK{YIQMt92l|D^(E_=$Rjw{b!QT@q!)ni
zR`|5oW9X5n$Wv+HVc@|^eX5yXnsHX<gx$-tTA9oOBadXir_JPm2Y^4ct-PoO&C)tI
zGolvqOIK@duBk!Vu9{g<3;i;gJ6?~-DQ&xz!jvD&4!U-s8Os(*#?k2}f30SEXA#=i
z1-qUX+K`{!((H5w7<t$~ygD!D1{~X6)KX%$qrgY#L_{M_7A<1csY*MfP@XcB#Jxr~
zJS8&7goVS)VKE|4(h_Xlc{z{c$ApZs7riZ_QKdV_uW-M~u~<J-*#Z0?VzcZp8)p-w
zus7J7><CN2I>8PF3UX~a6)MwxDE0HaPjyrlI!;jX{6Kvuh*8ej?;85ekN$?5uuCiS
zBTvvVG+XTxAO{m@bvM#Jr)z6J><&E22D|vq?Y?Vkbo_DijopiF$2PET#<s%v*srlI
z{B2SKJ79W>mZ8e<cESmGBON_l0n;T7>u=y$(ArYkv7@Ex`GL?QCc!_*KFrd&;n1r7
zqW-CFs9&fT)ZaU5gc&=gBz-D<EBz>aCw(vdOp0__x+47~U6sC(E(JNe@4cTT*n6*E
zVH4eoU1-&7pEV~_PRe`a7v+@vy!^5}8?Y3)UmlaE<h}6h3HHql{T;m+bPBU-O|^S1
z@dOw&4<!bj2G_<^#e}PL7FpY$lcrKO$i~?8Bd2y;oaL5^csibnCrF9!i%-PI;xhub
zp1k;8_$IKX1NHus6EHeD;B72SCCD@4ojP$=Mf3`Eo6yZ&eg@wTqDiZE);7u&SJ|(s
zuPF(9%D6IJ)klXF%`_Fy<tR3HxV^%Qqa?nAB97=m-uu2qcHInZ?ps8M|H3=#R%lzO
z6MgLv^}ib0hVV{&<};#;2lcwW;^(7C<OY#bI<VjS9qCKr-E_Cnc!2j+&nHAXA2%BR
zt~VMxUn2h&(Pi^LSpac(Y#S>R00009a7bBm000XU000XU0RWnu7ytkQtVu*cR5%f(
zQ%h)5K@^>N`81{eYe^ScQ%c)_i!NNb5K_An!G&E^QAAt_6$QaA+(=h-VRh455fL;B
zrhcJ*T8pTl_<=OoLZo7=lKzsWO-)|j|0UzOPaA_<2kx7hbI(0<=gxgx2*Lhmxc?1a
zVQjtEv^%+Adl=)*j0*{zY1oLc+*hiqSh75ZmuH~pb{psX8iF>JlS*sBHO|@TO0O%j
zij@TNy4@;qeiy+m-Oyhoe$3pseEsSe5l<XDzN0R%^{B7H*G7B6_^B$d>o%<!WsnwP
zt!h}4ieW9yCKu0!!{NH*%E~^tIaxZB+dDiw+`N!p?j{a0o!2z2-pQUmvZ8kaOf6?+
zO3e$Cz9te4@JTTKQ&!vay4f2D1bmrn{tj_-s@eNjK(`)}YJ75PH-U}=BAD0U#2)w(
zA7il=!ihs%2#(R7DvUfuJto`lui2uRg?6mHa7wCaKWBm;lJmLn=WO;1`G`YY&ZHX5
zmCk8Ui$eALz`(#pfF4qYxn<3KlU1~LJ^lUFgprRp#D#r}Mx!1_AS34o6~ms&<<y;j
zf*m{*3T+@mpz~VfBMxyP{Fe4kBdN0T(pO#4!adTF3}f*MtFWIJi@v8Lqjah>6sw>c
z9@hgeIJmNHS>|iTe7|cj^6c@wLQyP9l53Y&H4ctG85+`ca~cO|WV9xXe8j<JwD|a^
zo22ZdhVJ8I<MGyDu;~&o7Yg?(olHd{ky~*hb#xrwUQ_Eo0CoslF*iGVzNw*p5LYOc
zIuRP~*u+Gru`zf~lK46hsfo%5RneG9%wG&PZhM4a%W+a6Rt<o=tE;<qF1dIzo7YfS
z>jPfmeNNHuFQn3EF7)-$bFaRV6IaT>QNxF>g9+W}ceBj}i7ny3L{Bl6vVog8j4~;W
YU#@V)?0xW-3IG5A07*qoM6N<$g6IHrzW@LL
--- a/browser/themes/pinstripe/devtools/toolbox.css
+++ b/browser/themes/pinstripe/devtools/toolbox.css
@@ -49,17 +49,17 @@
   padding: 0 8px;
   margin: 0;
   width: 16px;
 }
 
 .command-button:hover {
   background-color: hsla(206,37%,4%,.2);
 }
-.command-button:hover:active {
+.command-button:hover:active, .command-button[checked=true]:not(:hover) {
   background-color: hsla(206,37%,4%,.4);
 }
 
 #command-button-responsive {
   list-style-image: url("chrome://browser/skin/devtools/command-responsivemode.png");
   -moz-image-region: rect(0px, 16px, 16px, 0px);
 }
 #command-button-responsive:hover {
--- a/browser/themes/pinstripe/jar.mn
+++ b/browser/themes/pinstripe/jar.mn
@@ -248,16 +248,17 @@ browser.jar:
   skin/classic/browser/devtools/dock-side.png               (devtools/dock-side.png)
   skin/classic/browser/devtools/floating-scrollbars.css     (devtools/floating-scrollbars.css)
 * skin/classic/browser/devtools/inspector.css               (devtools/inspector.css)
   skin/classic/browser/devtools/toolbox.css                 (devtools/toolbox.css)
   skin/classic/browser/devtools/tool-webconsole.png         (devtools/tool-webconsole.png)
   skin/classic/browser/devtools/tool-debugger.png           (devtools/tool-debugger.png)
   skin/classic/browser/devtools/tool-inspector.png          (devtools/tool-inspector.png)
   skin/classic/browser/devtools/tool-styleeditor.png        (devtools/tool-styleeditor.png)
+  skin/classic/browser/devtools/tool-profiler.png           (devtools/tool-profiler.png)
   skin/classic/browser/devtools/close.png                   (devtools/close.png)
   skin/classic/browser/devtools/undock.png                  (devtools/undock.png)
 #ifdef MOZ_SERVICES_SYNC
   skin/classic/browser/sync-throbber.png
   skin/classic/browser/sync-16.png
   skin/classic/browser/sync-32.png
   skin/classic/browser/sync-bg.png
   skin/classic/browser/sync-128.png
index 83842f236ef5ede55a268e67077532fc1764746f..8fa9c932bd9f26288f736f5d9820d8ff76f88569
GIT binary patch
literal 1383
zc$@)e1(^DYP)<h;3K|Lk000e1NJLTq000mG000mO1^@s6AM^iV000CeX+uL$Nkc;*
zP;zf(X>4Tx05}naRo`#hR1`jmZ&IWdKOk5~hl<6oRa0BJ8yc;~21%2p?MfD<>DVeH
z<T^KrsT&8|>9(p*dx19w`~g7O0}n_%Aq@s%d)fBDv`JHkDym6Hd+5XuAtvnwRpGmK
zVkc9?T=n|PIo~<wJLg{8L_J?=wVD}Kh?c9aozEndlcyGxo=u9<v(!ri)T`-EEs@L3
z5-!0N_s;9#9f}Cc?UC;OPWB_edW+oAi6T$HZWSGU8TbrQ%+zbPOBBBc`}k?M2Hf);
z@Y6N~0;>X-eVh__(Z?q}P9Z-Dj?gOW6|D%o20XmjW-qs4UjrD(li^iv8@eK9k+ZFm
zVRFymFOPAzG5-%Pn|1W;U4vNroTa&AxDScmEA~{ri9gr1^c?U@uwSpaNnw8l_>cP1
zd;)kMQS_;jeRSUEM_*s96y65j1$)tOrwdK{YIQMt92l|D^(E_=$Rjw{b!QT@q!)ni
zR`|5oW9X5n$Wv+HVc@|^eX5yXnsHX<gx$-tTA9oOBadXir_JPm2Y^4ct-PoO&C)tI
zGolvqOIK@duBk!Vu9{g<3;i;gJ6?~-DQ&xz!jvD&4!U-s8Os(*#?k2}f30SEXA#=i
z1-qUX+K`{!((H5w7<t$~ygD!D1{~X6)KX%$qrgY#L_{M_7A<1csY*MfP@XcB#Jxr~
zJS8&7goVS)VKE|4(h_Xlc{z{c$ApZs7riZ_QKdV_uW-M~u~<J-*#Z0?VzcZp8)p-w
zus7J7><CN2I>8PF3UX~a6)MwxDE0HaPjyrlI!;jX{6Kvuh*8ej?;85ekN$?5uuCiS
zBTvvVG+XTxAO{m@bvM#Jr)z6J><&E22D|vq?Y?Vkbo_DijopiF$2PET#<s%v*srlI
z{B2SKJ79W>mZ8e<cESmGBON_l0n;T7>u=y$(ArYkv7@Ex`GL?QCc!_*KFrd&;n1r7
zqW-CFs9&fT)ZaU5gc&=gBz-D<EBz>aCw(vdOp0__x+47~U6sC(E(JNe@4cTT*n6*E
zVH4eoU1-&7pEV~_PRe`a7v+@vy!^5}8?Y3)UmlaE<h}6h3HHql{T;m+bPBU-O|^S1
z@dOw&4<!bj2G_<^#e}PL7FpY$lcrKO$i~?8Bd2y;oaL5^csibnCrF9!i%-PI;xhub
zp1k;8_$IKX1NHus6EHeD;B72SCCD@4ojP$=Mf3`Eo6yZ&eg@wTqDiZE);7u&SJ|(s
zuPF(9%D6IJ)klXF%`_Fy<tR3HxV^%Qqa?nAB97=m-uu2qcHInZ?ps8M|H3=#R%lzO
z6MgLv^}ib0hVV{&<};#;2lcwW;^(7C<OY#bI<VjS9qCKr-E_Cnc!2j+&nHAXA2%BR
zt~VMxUn2h&(Pi^LSpac(Y#S>R00009a7bBm000XU000XU0RWnu7ytkO=}AOER5%f(
zQLzfbKn%TH3qrL{#X+20{KI~xyStNgP*FkK8k6Fcm!k@$2chrYdr6ZVF|(U;{FG$P
zom?Qr4j#|;NW@FzV$4j}Y?f?#UUv}U{;bbsS*~4C<U^sFdV;^(A6`VPq%t9gST*$^
zXLrEVi1_naI!!!%`vPnM=ZuI(z{V37&>3<t8~D{~B+V-@ah<)|{R16<Gr{Ts%9#Qr
zBeYF(Ktqg^G}$2+kI<0!D>1Y~@zu-@Ob72bU%aQe^T`(ff>=Fx@7_ERM5~1>SSI;%
pDor`vg2NOD<d|=~gE6_C_XB1vk}(x*QltO?002ovPDHLkV1lF4lNA5}
index 9a940bc1e4e50a63d1522fc5604cb2cf44b05e82..1c12355138bd03b6ed7529809e61b601530b624f
GIT binary patch
literal 1415
zc$@);1$g?2P)<h;3K|Lk000e1NJLTq000mG000mO1^@s6AM^iV000CeX+uL$Nkc;*
zP;zf(X>4Tx05}naRo`#hR1`jmZ&IWdKOk5~hl<6oRa0BJ8yc;~21%2p?MfD<>DVeH
z<T^KrsT&8|>9(p*dx19w`~g7O0}n_%Aq@s%d)fBDv`JHkDym6Hd+5XuAtvnwRpGmK
zVkc9?T=n|PIo~<wJLg{8L_J?=wVD}Kh?c9aozEndlcyGxo=u9<v(!ri)T`-EEs@L3
z5-!0N_s;9#9f}Cc?UC;OPWB_edW+oAi6T$HZWSGU8TbrQ%+zbPOBBBc`}k?M2Hf);
z@Y6N~0;>X-eVh__(Z?q}P9Z-Dj?gOW6|D%o20XmjW-qs4UjrD(li^iv8@eK9k+ZFm
zVRFymFOPAzG5-%Pn|1W;U4vNroTa&AxDScmEA~{ri9gr1^c?U@uwSpaNnw8l_>cP1
zd;)kMQS_;jeRSUEM_*s96y65j1$)tOrwdK{YIQMt92l|D^(E_=$Rjw{b!QT@q!)ni
zR`|5oW9X5n$Wv+HVc@|^eX5yXnsHX<gx$-tTA9oOBadXir_JPm2Y^4ct-PoO&C)tI
zGolvqOIK@duBk!Vu9{g<3;i;gJ6?~-DQ&xz!jvD&4!U-s8Os(*#?k2}f30SEXA#=i
z1-qUX+K`{!((H5w7<t$~ygD!D1{~X6)KX%$qrgY#L_{M_7A<1csY*MfP@XcB#Jxr~
zJS8&7goVS)VKE|4(h_Xlc{z{c$ApZs7riZ_QKdV_uW-M~u~<J-*#Z0?VzcZp8)p-w
zus7J7><CN2I>8PF3UX~a6)MwxDE0HaPjyrlI!;jX{6Kvuh*8ej?;85ekN$?5uuCiS
zBTvvVG+XTxAO{m@bvM#Jr)z6J><&E22D|vq?Y?Vkbo_DijopiF$2PET#<s%v*srlI
z{B2SKJ79W>mZ8e<cESmGBON_l0n;T7>u=y$(ArYkv7@Ex`GL?QCc!_*KFrd&;n1r7
zqW-CFs9&fT)ZaU5gc&=gBz-D<EBz>aCw(vdOp0__x+47~U6sC(E(JNe@4cTT*n6*E
zVH4eoU1-&7pEV~_PRe`a7v+@vy!^5}8?Y3)UmlaE<h}6h3HHql{T;m+bPBU-O|^S1
z@dOw&4<!bj2G_<^#e}PL7FpY$lcrKO$i~?8Bd2y;oaL5^csibnCrF9!i%-PI;xhub
zp1k;8_$IKX1NHus6EHeD;B72SCCD@4ojP$=Mf3`Eo6yZ&eg@wTqDiZE);7u&SJ|(s
zuPF(9%D6IJ)klXF%`_Fy<tR3HxV^%Qqa?nAB97=m-uu2qcHInZ?ps8M|H3=#R%lzO
z6MgLv^}ib0hVV{&<};#;2lcwW;^(7C<OY#bI<VjS9qCKr-E_Cnc!2j+&nHAXA2%BR
zt~VMxUn2h&(Pi^LSpac(Y#S>R00009a7bBm000XU000XU0RWnu7ytkP2}wjjR5%f(
zQA-YjKn$G%YT|E&EQ~9+-j5S<1rK22N?oV{jq<52PH{SkCWa<u+Sm8mp#z9$`dH9M
z%-Y@!BKHQjN4*EYxr_NRB3#F#aMj_w1M%Wfp3^j4nmC?`bI$msxM%tOx#L*)d;)*O
zZqMp884VVMbs&(D=vbH4mU-bI+Kh)$EE>H7gvu2&sm%$eZM~gi$J7o8S4D?MjZJNH
z#qk@Ij>ruO?<OmbYJh;MC><=`PWCyf0etxd#>v}X@Hu*k?*>zuY){U4dJRF4*w~hg
zQ8#EBywFo-UG)=GVHB=8WzGkt%{WhP0FtSRk#70JPAoa(!cYwa{O9y{@Q=3Tz5x9H
Vj0(v1c&q>b002ovPDHLkV1mI<q`UwC
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..8f30f12a9d52bd62ee7b10180dd66fd8f45e72b7
GIT binary patch
literal 1834
zc$@($2i5qAP)<h;3K|Lk000e1NJLTq000mG000mO1^@s6AM^iV000CeX+uL$Nkc;*
zP;zf(X>4Tx05}naRo`#hR1`jmZ&IWdKOk5~hl<6oRa0BJ8yc;~21%2p?MfD<>DVeH
z<T^KrsT&8|>9(p*dx19w`~g7O0}n_%Aq@s%d)fBDv`JHkDym6Hd+5XuAtvnwRpGmK
zVkc9?T=n|PIo~<wJLg{8L_J?=wVD}Kh?c9aozEndlcyGxo=u9<v(!ri)T`-EEs@L3
z5-!0N_s;9#9f}Cc?UC;OPWB_edW+oAi6T$HZWSGU8TbrQ%+zbPOBBBc`}k?M2Hf);
z@Y6N~0;>X-eVh__(Z?q}P9Z-Dj?gOW6|D%o20XmjW-qs4UjrD(li^iv8@eK9k+ZFm
zVRFymFOPAzG5-%Pn|1W;U4vNroTa&AxDScmEA~{ri9gr1^c?U@uwSpaNnw8l_>cP1
zd;)kMQS_;jeRSUEM_*s96y65j1$)tOrwdK{YIQMt92l|D^(E_=$Rjw{b!QT@q!)ni
zR`|5oW9X5n$Wv+HVc@|^eX5yXnsHX<gx$-tTA9oOBadXir_JPm2Y^4ct-PoO&C)tI
zGolvqOIK@duBk!Vu9{g<3;i;gJ6?~-DQ&xz!jvD&4!U-s8Os(*#?k2}f30SEXA#=i
z1-qUX+K`{!((H5w7<t$~ygD!D1{~X6)KX%$qrgY#L_{M_7A<1csY*MfP@XcB#Jxr~
zJS8&7goVS)VKE|4(h_Xlc{z{c$ApZs7riZ_QKdV_uW-M~u~<J-*#Z0?VzcZp8)p-w
zus7J7><CN2I>8PF3UX~a6)MwxDE0HaPjyrlI!;jX{6Kvuh*8ej?;85ekN$?5uuCiS
zBTvvVG+XTxAO{m@bvM#Jr)z6J><&E22D|vq?Y?Vkbo_DijopiF$2PET#<s%v*srlI
z{B2SKJ79W>mZ8e<cESmGBON_l0n;T7>u=y$(ArYkv7@Ex`GL?QCc!_*KFrd&;n1r7
zqW-CFs9&fT)ZaU5gc&=gBz-D<EBz>aCw(vdOp0__x+47~U6sC(E(JNe@4cTT*n6*E
zVH4eoU1-&7pEV~_PRe`a7v+@vy!^5}8?Y3)UmlaE<h}6h3HHql{T;m+bPBU-O|^S1
z@dOw&4<!bj2G_<^#e}PL7FpY$lcrKO$i~?8Bd2y;oaL5^csibnCrF9!i%-PI;xhub
zp1k;8_$IKX1NHus6EHeD;B72SCCD@4ojP$=Mf3`Eo6yZ&eg@wTqDiZE);7u&SJ|(s
zuPF(9%D6IJ)klXF%`_Fy<tR3HxV^%Qqa?nAB97=m-uu2qcHInZ?ps8M|H3=#R%lzO
z6MgLv^}ib0hVV{&<};#;2lcwW;^(7C<OY#bI<VjS9qCKr-E_Cnc!2j+&nHAXA2%BR
zt~VMxUn2h&(Pi^LSpac(Y#S>R00009a7bBm000XU000XU0RWnu7ytkQtVu*cR5%f(
zQ%h)5K@^>N`81{eYe^ScQ%c)_i!NNb5K_An!G&E^QAAt_6$QaA+(=h-VRh455fL;B
zrhcJ*T8pTl_<=OoLZo7=lKzsWO-)|j|0UzOPaA_<2kx7hbI(0<=gxgx2*Lhmxc?1a
zVQjtEv^%+Adl=)*j0*{zY1oLc+*hiqSh75ZmuH~pb{psX8iF>JlS*sBHO|@TO0O%j
zij@TNy4@;qeiy+m-Oyhoe$3pseEsSe5l<XDzN0R%^{B7H*G7B6_^B$d>o%<!WsnwP
zt!h}4ieW9yCKu0!!{NH*%E~^tIaxZB+dDiw+`N!p?j{a0o!2z2-pQUmvZ8kaOf6?+
zO3e$Cz9te4@JTTKQ&!vay4f2D1bmrn{tj_-s@eNjK(`)}YJ75PH-U}=BAD0U#2)w(
zA7il=!ihs%2#(R7DvUfuJto`lui2uRg?6mHa7wCaKWBm;lJmLn=WO;1`G`YY&ZHX5
zmCk8Ui$eALz`(#pfF4qYxn<3KlU1~LJ^lUFgprRp#D#r}Mx!1_AS34o6~ms&<<y;j
zf*m{*3T+@mpz~VfBMxyP{Fe4kBdN0T(pO#4!adTF3}f*MtFWIJi@v8Lqjah>6sw>c
z9@hgeIJmNHS>|iTe7|cj^6c@wLQyP9l53Y&H4ctG85+`ca~cO|WV9xXe8j<JwD|a^
zo22ZdhVJ8I<MGyDu;~&o7Yg?(olHd{ky~*hb#xrwUQ_Eo0CoslF*iGVzNw*p5LYOc
zIuRP~*u+Gru`zf~lK46hsfo%5RneG9%wG&PZhM4a%W+a6Rt<o=tE;<qF1dIzo7YfS
z>jPfmeNNHuFQn3EF7)-$bFaRV6IaT>QNxF>g9+W}ceBj}i7ny3L{Bl6vVog8j4~;W
YU#@V)?0xW-3IG5A07*qoM6N<$g6IHrzW@LL
--- a/browser/themes/winstripe/devtools/toolbox.css
+++ b/browser/themes/winstripe/devtools/toolbox.css
@@ -65,17 +65,17 @@
   margin: 0;
   padding: 0 8px;
   width: 16px;
 }
 
 .command-button:hover {
   background-color: hsla(206,37%,4%,.2);
 }
-.command-button:hover:active {
+.command-button:hover:active, .command-button[checked=true]:not(:hover) {
   background-color: hsla(206,37%,4%,.4);
 }
 
 #command-button-responsive {
   list-style-image: url("chrome://browser/skin/devtools/command-responsivemode.png");
   -moz-image-region: rect(0px, 16px, 16px, 0px);
 }
 #command-button-responsive:hover {
--- a/browser/themes/winstripe/jar.mn
+++ b/browser/themes/winstripe/jar.mn
@@ -203,16 +203,17 @@ browser.jar:
         skin/classic/browser/devtools/dock-side.png                 (devtools/dock-side.png)
         skin/classic/browser/devtools/floating-scrollbars.css       (devtools/floating-scrollbars.css)
         skin/classic/browser/devtools/inspector.css                 (devtools/inspector.css)
         skin/classic/browser/devtools/toolbox.css                   (devtools/toolbox.css)
         skin/classic/browser/devtools/tool-webconsole.png           (devtools/tool-webconsole.png)
         skin/classic/browser/devtools/tool-debugger.png             (devtools/tool-debugger.png)
         skin/classic/browser/devtools/tool-inspector.png            (devtools/tool-inspector.png)
         skin/classic/browser/devtools/tool-styleeditor.png          (devtools/tool-styleeditor.png)
+        skin/classic/browser/devtools/tool-profiler.png             (devtools/tool-profiler.png)
         skin/classic/browser/devtools/close.png                     (devtools/close.png)
         skin/classic/browser/devtools/undock.png                    (devtools/undock.png)
 #ifdef MOZ_SERVICES_SYNC
         skin/classic/browser/sync-throbber.png
         skin/classic/browser/sync-16.png
         skin/classic/browser/sync-32.png
         skin/classic/browser/sync-128.png
         skin/classic/browser/sync-bg.png
@@ -424,16 +425,17 @@ browser.jar:
         skin/classic/aero/browser/devtools/dock-side.png             (devtools/dock-side.png)
         skin/classic/aero/browser/devtools/floating-scrollbars.css   (devtools/floating-scrollbars.css)
         skin/classic/aero/browser/devtools/inspector.css             (devtools/inspector.css)
         skin/classic/aero/browser/devtools/toolbox.css               (devtools/toolbox.css)
         skin/classic/aero/browser/devtools/tool-webconsole.png       (devtools/tool-webconsole.png)
         skin/classic/aero/browser/devtools/tool-debugger.png         (devtools/tool-debugger.png)
         skin/classic/aero/browser/devtools/tool-inspector.png        (devtools/tool-inspector.png)
         skin/classic/aero/browser/devtools/tool-styleeditor.png      (devtools/tool-styleeditor.png)
+        skin/classic/aero/browser/devtools/tool-profiler.png         (devtools/tool-profiler.png)
         skin/classic/aero/browser/devtools/close.png                 (devtools/close.png)
         skin/classic/aero/browser/devtools/undock.png                (devtools/undock.png)
 #ifdef MOZ_SERVICES_SYNC
         skin/classic/aero/browser/sync-throbber.png
         skin/classic/aero/browser/sync-16.png
         skin/classic/aero/browser/sync-32.png
         skin/classic/aero/browser/sync-128.png
         skin/classic/aero/browser/sync-bg.png
--- a/mobile/android/installer/package-manifest.in
+++ b/mobile/android/installer/package-manifest.in
@@ -421,21 +421,16 @@
 @BINPATH@/@PREF_DIR@/mobile.js
 @BINPATH@/@PREF_DIR@/mobile-branding.js
 @BINPATH@/@PREF_DIR@/channel-prefs.js
 @BINPATH@/greprefs.js
 @BINPATH@/defaults/autoconfig/platform.js
 @BINPATH@/defaults/autoconfig/prefcalls.js
 @BINPATH@/defaults/profile/prefs.js
 
-; Services (gre) prefs
-#ifdef MOZ_SERVICES_HEALTHREPORT
-@BINPATH@/defaults/pref/healthreport-prefs.js
-#endif
-
 ; [Layout Engine Resources]
 ; Style Sheets, Graphics and other Resources used by the layout engine. 
 @BINPATH@/res/EditorOverride.css
 @BINPATH@/res/contenteditable.css
 @BINPATH@/res/designmode.css
 @BINPATH@/res/TopLevelImageDocument.css
 @BINPATH@/res/TopLevelVideoDocument.css
 @BINPATH@/res/table-add-column-after-active.gif
--- a/mobile/xul/installer/package-manifest.in
+++ b/mobile/xul/installer/package-manifest.in
@@ -505,19 +505,16 @@
 @BINPATH@/defaults/autoconfig/platform.js
 @BINPATH@/defaults/autoconfig/prefcalls.js
 @BINPATH@/defaults/profile/prefs.js
 
 ; Services (gre) prefs
 #ifdef MOZ_SERVICES_SYNC
 @BINPATH@/defaults/pref/services-sync.js
 #endif
-#ifdef MOZ_SERVICES_HEALTHREPORT
-@BINPATH@/defaults/pref/healthreport-prefs.js
-#endif
 
 ; [Layout Engine Resources]
 ; Style Sheets, Graphics and other Resources used by the layout engine. 
 @BINPATH@/res/EditorOverride.css
 @BINPATH@/res/contenteditable.css
 @BINPATH@/res/designmode.css
 @BINPATH@/res/TopLevelImageDocument.css
 @BINPATH@/res/TopLevelVideoDocument.css
--- a/modules/libpref/src/Makefile.in
+++ b/modules/libpref/src/Makefile.in
@@ -35,24 +35,29 @@ include $(topsrcdir)/ipc/chromium/chromi
 include $(topsrcdir)/config/rules.mk
 
 GARBAGE		+= $(addprefix $(DIST)/bin/defaults/pref/, \
 			mailnews.js editor.js \
 			aix.js unix.js winpref.js os2prefs.js)
 
 GARBAGE		+= greprefs.js
 
-GREPREF_FILES = $(topsrcdir)/netwerk/base/public/security-prefs.js $(srcdir)/init/all.js
+# TODO bug 813259 external files should be defined near their location in the source tree.
+grepref_files = $(topsrcdir)/netwerk/base/public/security-prefs.js $(srcdir)/init/all.js
+
+ifdef MOZ_SERVICES_HEALTHREPORT
+grepref_files += $(topsrcdir)/services/healthreport/healthreport-prefs.js
+endif
 
 # Optimizer bug with GCC 3.2.2 on OS/2
 ifeq ($(OS_ARCH), OS2)
 nsPrefService.$(OBJ_SUFFIX): nsPrefService.cpp
 	$(REPORT_BUILD)
 	@$(MAKE_DEPS_AUTO_CXX)
 	$(ELOG) $(CCC) $(OUTOPTION)$@ -c $(COMPILE_CXXFLAGS:-O2=-O1) $(_VPATH_SRCS)
 endif
 
 
-greprefs.js: $(GREPREF_FILES)
+greprefs.js: $(grepref_files)
 	$(PYTHON) $(topsrcdir)/config/Preprocessor.py $(PREF_PPFLAGS) $(DEFINES) $(ACDEFINES) $(XULPPFLAGS) $^ > $@
 
 libs:: greprefs.js
 	$(INSTALL) $^ $(DIST)/bin/
--- a/services/Makefile.in
+++ b/services/Makefile.in
@@ -20,17 +20,13 @@ endif
 ifdef MOZ_SERVICES_HEALTHREPORT
 PARALLEL_DIRS += healthreport
 endif
 
 ifdef MOZ_SERVICES_METRICS
 PARALLEL_DIRS += metrics
 endif
 
-ifdef MOZ_SERVICES_NOTIFICATIONS
-PARALLEL_DIRS += notifications
-endif
-
 ifdef MOZ_SERVICES_SYNC
 PARALLEL_DIRS += sync
 endif
 
 include $(topsrcdir)/config/rules.mk
--- a/services/healthreport/HealthReportComponents.manifest
+++ b/services/healthreport/HealthReportComponents.manifest
@@ -4,12 +4,15 @@
 #   mobile/xul:     {a23983c0-fd0e-11dc-95ff-0800200c9a66}
 #   suite (comm):   {92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}
 #   metro browser:  {99bceaaa-e3c6-48c1-b981-ef9b46b67d60}
 
 component {e354c59b-b252-4040-b6dd-b71864e3e35c} HealthReportService.js
 contract @mozilla.org/healthreport/service;1 {e354c59b-b252-4040-b6dd-b71864e3e35c}
 category app-startup HealthReportService service,@mozilla.org/healthreport/service;1 application={3c2e2abc-06d4-11e1-ac3b-374f68613e61} application={ec8030f7-c20a-464f-9b0e-13a3a9e97384} application={aa3c5121-dab2-40e2-81ca-7ea25febc110} application={a23983c0-fd0e-11dc-95ff-0800200c9a66}
 
+category healthreport-js-provider AddonsProvider resource://gre/modules/services/healthreport/providers.jsm
 category healthreport-js-provider AppInfoProvider resource://gre/modules/services/healthreport/providers.jsm
+category healthreport-js-provider CrashesProvider resource://gre/modules/services/healthreport/providers.jsm
 category healthreport-js-provider SysInfoProvider resource://gre/modules/services/healthreport/providers.jsm
 category healthreport-js-provider ProfileMetadataProvider resource://gre/modules/services/healthreport/profile.jsm
+category healthreport-js-provider SessionsProvider resource://gre/modules/services/healthreport/providers.jsm
 
--- a/services/healthreport/HealthReportService.js
+++ b/services/healthreport/HealthReportService.js
@@ -5,127 +5,146 @@
 "use strict";
 
 const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://services-common/preferences.js");
 
 
-const INITIAL_STARTUP_DELAY_MSEC = 10 * 1000;
 const BRANCH = "healthreport.";
-const JS_PROVIDERS_CATEGORY = "healthreport-js-provider";
-
+const DEFAULT_LOAD_DELAY_MSEC = 10 * 1000;
 
 /**
  * The Firefox Health Report XPCOM service.
  *
- * This instantiates an instance of HealthReporter (assuming it is enabled)
- * and starts it upon application startup.
+ * External consumers will be interested in the "reporter" property of this
+ * service. This property is a `HealthReporter` instance that powers the
+ * service. The property may be null if the Health Report service is not
+ * enabled.
+ *
+ * EXAMPLE USAGE
+ * =============
+ *
+ * let reporter = Cc["@mozilla.org/healthreport/service;1"]
+ *                  .getService(Ci.nsISupports)
+ *                  .wrappedJSObject
+ *                  .reporter;
  *
- * One can obtain a reference to the underlying HealthReporter instance by
- * accessing .reporter. If this property is null, the reporter isn't running
- * yet or has been disabled.
+ * if (reporter.haveRemoteData) {
+ *   // ...
+ * }
+ *
+ * IMPLEMENTATION NOTES
+ * ====================
+ *
+ * In order to not adversely impact application start time, the `HealthReporter`
+ * instance is not initialized until a few seconds after "final-ui-startup."
+ * The exact delay is configurable via preferences so it can be adjusted with
+ * a hotfix extension if the default value is ever problematic.
+ *
+ * Shutdown of the `HealthReporter` instance is handled completely within the
+ * instance (it registers observers on initialization). See the notes on that
+ * type for more.
  */
 this.HealthReportService = function HealthReportService() {
   this.wrappedJSObject = this;
 
-  this.prefs = new Preferences(BRANCH);
+  this._prefs = new Preferences(BRANCH);
+
   this._reporter = null;
 }
 
 HealthReportService.prototype = {
   classID: Components.ID("{e354c59b-b252-4040-b6dd-b71864e3e35c}"),
 
   QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
                                          Ci.nsISupportsWeakReference]),
 
   observe: function observe(subject, topic, data) {
     // If the background service is disabled, don't do anything.
-    if (!this.prefs.get("serviceEnabled", true)) {
+    if (!this._prefs.get("service.enabled", true)) {
       return;
     }
 
     let os = Cc["@mozilla.org/observer-service;1"]
                .getService(Ci.nsIObserverService);
 
     switch (topic) {
       case "app-startup":
         os.addObserver(this, "final-ui-startup", true);
         break;
 
       case "final-ui-startup":
         os.removeObserver(this, "final-ui-startup");
-        os.addObserver(this, "quit-application", true);
+
+        let delayInterval = this._prefs.get("service.loadDelayMsec") ||
+                            DEFAULT_LOAD_DELAY_MSEC;
 
         // Delay service loading a little more so things have an opportunity
         // to cool down first.
         this.timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
         this.timer.initWithCallback({
           notify: function notify() {
             // Side effect: instantiates the reporter instance if not already
             // accessed.
             let reporter = this.reporter;
             delete this.timer;
           }.bind(this),
-        }, INITIAL_STARTUP_DELAY_MSEC, this.timer.TYPE_ONE_SHOT);
-
-        break;
+        }, delayInterval, this.timer.TYPE_ONE_SHOT);
 
-      case "quit-application-granted":
-        if (this.reporter) {
-          this.reporter.stop();
-        }
-
-        os.removeObserver(this, "quit-application");
         break;
     }
   },
 
   /**
    * The HealthReporter instance associated with this service.
+   *
+   * If the service is disabled, this will return null.
+   *
+   * The obtained instance may not be fully initialized.
    */
   get reporter() {
-    if (!this.prefs.get("serviceEnabled", true)) {
+    if (!this._prefs.get("service.enabled", true)) {
       return null;
     }
 
     if (this._reporter) {
       return this._reporter;
     }
 
+    let ns = {};
     // Lazy import so application startup isn't adversely affected.
-    let ns = {};
+    Cu.import("resource://gre/modules/Task.jsm", ns);
+    Cu.import("resource://gre/modules/services/healthreport/healthreporter.jsm", ns);
     Cu.import("resource://services-common/log4moz.js", ns);
-    Cu.import("resource://gre/modules/services/healthreport/healthreporter.jsm", ns);
 
     // How many times will we rewrite this code before rolling it up into a
     // generic module? See also bug 451283.
     const LOGGERS = [
-      "Metrics",
       "Services.HealthReport",
       "Services.Metrics",
       "Services.BagheeraClient",
+      "Sqlite.Connection.healthreport",
     ];
 
     let prefs = new Preferences(BRANCH + "logging.");
     if (prefs.get("consoleEnabled", true)) {
       let level = prefs.get("consoleLevel", "Warn");
       let appender = new ns.Log4Moz.ConsoleAppender();
       appender.level = ns.Log4Moz.Level[level] || ns.Log4Moz.Level.Warn;
 
       for (let name of LOGGERS) {
         let logger = ns.Log4Moz.repository.getLogger(name);
         logger.addAppender(appender);
       }
     }
 
+    // The reporter initializes in the background.
     this._reporter = new ns.HealthReporter(BRANCH);
-    this._reporter.registerProvidersFromCategoryManager(JS_PROVIDERS_CATEGORY);
-    this._reporter.start();
 
     return this._reporter;
   },
 };
 
 Object.freeze(HealthReportService.prototype);
 
 this.NSGetFactory = XPCOMUtils.generateNSGetFactory([HealthReportService]);
--- a/services/healthreport/Makefile.in
+++ b/services/healthreport/Makefile.in
@@ -13,27 +13,26 @@ modules := \
   healthreporter.jsm \
   policy.jsm \
   profile.jsm \
   providers.jsm \
   $(NULL)
 
 testing_modules := \
   mocks.jsm \
+  utils.jsm \
   $(NULL)
 
 TEST_DIRS += tests
 
 MODULES_FILES := $(modules)
 MODULES_DEST = $(FINAL_TARGET)/modules/services/healthreport
 INSTALL_TARGETS += MODULES
 
 TESTING_JS_MODULES := $(addprefix modules-testing/,$(testing_modules))
 TESTING_JS_MODULE_DIR := services/healthreport
 
 EXTRA_COMPONENTS := \
   HealthReportComponents.manifest \
   HealthReportService.js \
   $(NULL)
 
-PREF_JS_EXPORTS := healthreport-prefs.js
-
 include $(topsrcdir)/config/rules.mk
--- a/services/healthreport/healthreport-prefs.js
+++ b/services/healthreport/healthreport-prefs.js
@@ -1,22 +1,24 @@
 /* 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/. */
 
 pref("healthreport.documentServerURI", "https://data.mozilla.com/");
 pref("healthreport.documentServerNamespace", "metrics");
-pref("healthreport.serviceEnabled", false);
 pref("healthreport.logging.consoleEnabled", true);
 pref("healthreport.logging.consoleLevel", "Warn");
 pref("healthreport.policy.currentDaySubmissionFailureCount", 0);
 pref("healthreport.policy.dataSubmissionEnabled", true);
 pref("healthreport.policy.dataSubmissionPolicyAccepted", false);
 pref("healthreport.policy.dataSubmissionPolicyBypassAcceptance", false);
 pref("healthreport.policy.dataSubmissionPolicyNotifiedTime", "0");
 pref("healthreport.policy.dataSubmissionPolicyResponseType", "");
 pref("healthreport.policy.dataSubmissionPolicyResponseTime", "0");
 pref("healthreport.policy.firstRunTime", "0");
 pref("healthreport.policy.lastDataSubmissionFailureTime", "0");
 pref("healthreport.policy.lastDataSubmissionRequestedTime", "0");
 pref("healthreport.policy.lastDataSubmissionSuccessfulTime", "0");
 pref("healthreport.policy.nextDataSubmissionTime", "0");
+pref("healthreport.service.enabled", true);
+pref("healthreport.service.loadDelayMsec", 10000);
+pref("healthreport.service.providerCategories", "healthreport-js-provider");
 
--- a/services/healthreport/healthreporter.jsm
+++ b/services/healthreport/healthreporter.jsm
@@ -3,74 +3,143 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 this.EXPORTED_SYMBOLS = ["HealthReporter"];
 
 const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
 
+Cu.import("resource://services-common/async.js");
 Cu.import("resource://services-common/bagheeraclient.js");
 Cu.import("resource://services-common/log4moz.js");
 Cu.import("resource://services-common/observers.js");
 Cu.import("resource://services-common/preferences.js");
 Cu.import("resource://services-common/utils.js");
 Cu.import("resource://gre/modules/commonjs/promise/core.js");
+Cu.import("resource://gre/modules/Metrics.jsm");
 Cu.import("resource://gre/modules/osfile.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/services/healthreport/policy.jsm");
-Cu.import("resource://gre/modules/services/metrics/collector.jsm");
 
 
 // Oldest year to allow in date preferences. This module was implemented in
 // 2012 and no dates older than that should be encountered.
 const OLDEST_ALLOWED_YEAR = 2012;
 
+const DAYS_IN_PAYLOAD = 180;
+const MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000;
+
+const DEFAULT_DATABASE_NAME = "healthreport.sqlite";
+
 
 /**
- * Coordinates collection and submission of metrics.
+ * Coordinates collection and submission of health report metrics.
  *
  * This is the main type for Firefox Health Report. It glues all the
  * lower-level components (such as collection and submission) together.
  *
  * An instance of this type is created as an XPCOM service. See
  * HealthReportService.js and HealthReportComponents.manifest.
  *
  * It is theoretically possible to have multiple instances of this running
  * in the application. For example, this type may one day handle submission
  * of telemetry data as well. However, there is some moderate coupling between
  * this type and *the* Firefox Health Report (e.g. the policy). This could
  * be abstracted if needed.
  *
+ * IMPLEMENTATION NOTES
+ * ====================
+ *
+ * Initialization and shutdown are somewhat complicated and worth explaining
+ * in extra detail.
+ *
+ * The complexity is driven by the requirements of SQLite connection management.
+ * Once you have a SQLite connection, it isn't enough to just let the
+ * application shut down. If there is an open connection or if there are
+ * outstanding SQL statements come XPCOM shutdown time, Storage will assert.
+ * On debug builds you will crash. On release builds you will get a shutdown
+ * hang. This must be avoided!
+ *
+ * During initialization, the second we create a SQLite connection (via
+ * Metrics.Storage) we register observers for application shutdown. The
+ * "quit-application" notification initiates our shutdown procedure. The
+ * subsequent "profile-do-change" notification ensures it has completed.
+ *
+ * The handler for "profile-do-change" may result in event loop spinning. This
+ * is because of race conditions between our shutdown code and application
+ * shutdown.
+ *
+ * All of our shutdown routines are async. There is the potential that these
+ * async functions will not complete before XPCOM shutdown. If they don't
+ * finish in time, we could get assertions in Storage. Our solution is to
+ * initiate storage early in the shutdown cycle ("quit-application").
+ * Hopefully all the async operations have completed by the time we reach
+ * "profile-do-change." If so, great. If not, we spin the event loop until
+ * they have completed, avoiding potential race conditions.
+ *
  * @param branch
  *        (string) The preferences branch to use for state storage. The value
  *        must end with a period (.).
  */
-this.HealthReporter = function HealthReporter(branch) {
+function HealthReporter(branch) {
   if (!branch.endsWith(".")) {
-    throw new Error("Branch argument must end with a period (.): " + branch);
+    throw new Error("Branch must end with a period (.): " + branch);
   }
 
   this._log = Log4Moz.repository.getLogger("Services.HealthReport.HealthReporter");
+  this._log.info("Initializing health reporter instance against " + branch);
 
   this._prefs = new Preferences(branch);
 
-  let policyBranch = new Preferences(branch + "policy.");
-  this._policy = new HealthReportPolicy(policyBranch, this);
-  this._collector = new MetricsCollector();
-
   if (!this.serverURI) {
     throw new Error("No server URI defined. Did you forget to define the pref?");
   }
 
   if (!this.serverNamespace) {
     throw new Error("No server namespace defined. Did you forget a pref?");
   }
+
+  this._dbName = this._prefs.get("dbName") || DEFAULT_DATABASE_NAME;
+
+  let policyBranch = new Preferences(branch + "policy.");
+  this._policy = new HealthReportPolicy(policyBranch, this);
+
+  this._storage = null;
+  this._storageInProgress = false;
+  this._collector = null;
+  this._collectorInProgress = false;
+  this._initialized = false;
+  this._initializeHadError = false;
+  this._initializedDeferred = Promise.defer();
+  this._shutdownRequested = false;
+  this._shutdownInitiated = false;
+  this._shutdownComplete = false;
+  this._shutdownCompleteCallback = null;
+
+  this._ensureDirectoryExists(this._stateDir)
+      .then(this._onStateDirCreated.bind(this),
+            this._onInitError.bind(this));
+
 }
 
-HealthReporter.prototype = {
+HealthReporter.prototype = Object.freeze({
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]),
+
+  /**
+   * Whether the service is fully initialized and running.
+   *
+   * If this is false, it is not safe to call most functions.
+   */
+  get initialized() {
+    return this._initialized;
+  },
+
   /**
    * When we last successfully submitted data to the server.
    *
    * This is sent as part of the upload. This is redundant with similar data
    * in the policy because we like the modules to be loosely coupled and the
    * similar data in the policy is only used for forensic purposes.
    */
   get lastPingDate() {
@@ -141,147 +210,373 @@ HealthReporter.prototype = {
     this._prefs.set("lastSubmitID", value || "");
   },
 
   /**
    * Whether remote data is currently stored.
    *
    * @return bool
    */
-  haveRemoteData: function haveRemoteData() {
+  haveRemoteData: function () {
     return !!this.lastSubmitID;
   },
 
+  //----------------------------------------------------
+  // SERVICE CONTROL FUNCTIONS
+  //
+  // You shouldn't need to call any of these externally.
+  //----------------------------------------------------
+
+  _onInitError: function (error) {
+    this._log.error("Error during initialization: " +
+                    CommonUtils.exceptionStr(error));
+    this._initializeHadError = true;
+    this._initiateShutdown();
+    this._initializedDeferred.reject(error);
+
+    // FUTURE consider poisoning prototype's functions so calls fail with a
+    // useful error message.
+  },
+
+  _onStateDirCreated: function () {
+    // As soon as we have could storage, we need to register cleanup or
+    // else bad things happen on shutdown.
+    Services.obs.addObserver(this, "quit-application", false);
+    Services.obs.addObserver(this, "profile-before-change", false);
+
+    this._storageInProgress = true;
+    Metrics.Storage(this._dbName).then(this._onStorageCreated.bind(this),
+                                       this._onInitError.bind(this));
+  },
+
+  // Called when storage has been opened.
+  _onStorageCreated: function (storage) {
+    this._log.info("Storage initialized.");
+    this._storage = storage;
+    this._storageInProgress = false;
+
+    if (this._shutdownRequested) {
+      this._initiateShutdown();
+      return;
+    }
+
+    Task.spawn(this._initializeCollector.bind(this))
+        .then(this._onCollectorInitialized.bind(this),
+              this._onInitError.bind(this));
+  },
+
+  _initializeCollector: function () {
+    if (this._collector) {
+      throw new Error("Collector has already been initialized.");
+    }
+
+    this._log.info("Initializing collector.");
+    this._collector = new Metrics.Collector(this._storage);
+    this._collectorInProgress = true;
+
+    let catString = this._prefs.get("service.providerCategories") || "";
+    if (catString.length) {
+      for (let category of catString.split(",")) {
+        yield this.registerProvidersFromCategoryManager(category);
+      }
+    }
+  },
+
+  _onCollectorInitialized: function () {
+    this._log.debug("Collector initialized.");
+    this._collectorInProgress = false;
+
+    if (this._shutdownRequested) {
+      this._initiateShutdown();
+      return;
+    }
+
+    this._policy.startPolling();
+    this._log.info("HealthReporter started.");
+    this._initialized = true;
+    Services.obs.addObserver(this, "idle-daily", false);
+    this._initializedDeferred.resolve(this);
+  },
+
+  // nsIObserver to handle shutdown.
+  observe: function (subject, topic, data) {
+    switch (topic) {
+      case "quit-application":
+        Services.obs.removeObserver(this, "quit-application");
+        this._initiateShutdown();
+        break;
+
+      case "profile-before-change":
+        Services.obs.removeObserver(this, "profile-before-change");
+        this._waitForShutdown();
+        break;
+
+      case "idle-daily":
+        this._performDailyMaintenance();
+        break;
+    }
+  },
+
+  _initiateShutdown: function () {
+    // Ensure we only begin the main shutdown sequence once.
+    if (this._shutdownInitiated) {
+      this._log.warn("Shutdown has already been initiated. No-op.");
+      return;
+    }
+
+    this._log.info("Request to shut down.");
+
+    this._initialized = false;
+    this._shutdownRequested = true;
+
+    // Safe to call multiple times.
+    this._policy.stopPolling();
+
+    if (this._collectorInProgress) {
+      this._log.warn("Collector is in progress of initializing. Waiting to finish.");
+      return;
+    }
+
+    // If storage is in the process of initializing, we need to wait for it
+    // to finish before continuing. The initialization process will call us
+    // again once storage has initialized.
+    if (this._storageInProgress) {
+      this._log.warn("Storage is in progress of initializing. Waiting to finish.");
+      return;
+    }
+
+    this._log.warn("Initiating main shutdown procedure.");
+
+    // Everything from here must only be performed once or else race conditions
+    // could occur.
+    this._shutdownInitiated = true;
+
+    if (this._initialized) {
+      Services.obs.removeObserver(this, "idle-daily");
+    }
+
+    // If we have collectors, we need to shut down providers.
+    if (this._collector) {
+      let onShutdown = this._onCollectorShutdown.bind(this);
+      Task.spawn(this._shutdownCollector.bind(this))
+          .then(onShutdown, onShutdown);
+      return;
+    }
+
+    this._log.warn("Don't have collector. Proceeding to storage shutdown.");
+    this._shutdownStorage();
+  },
+
+  _shutdownCollector: function () {
+    this._log.info("Shutting down collector.");
+    for (let provider of this._collector.providers) {
+      try {
+        yield provider.shutdown();
+      } catch (ex) {
+        this._log.warn("Error when shutting down provider: " +
+                       CommonUtils.exceptionStr(ex));
+      }
+    }
+  },
+
+  _onCollectorShutdown: function () {
+    this._log.info("Collector shut down.");
+    this._collector = null;
+    this._shutdownStorage();
+  },
+
+  _shutdownStorage: function () {
+    if (!this._storage) {
+      this._onShutdownComplete();
+    }
+
+    this._log.info("Shutting down storage.");
+    let onClose = this._onStorageClose.bind(this);
+    this._storage.close().then(onClose, onClose);
+  },
+
+  _onStorageClose: function (error) {
+    this._log.info("Storage has been closed.");
+
+    if (error) {
+      this._log.warn("Error when closing storage: " +
+                     CommonUtils.exceptionStr(error));
+    }
+
+    this._storage = null;
+    this._onShutdownComplete();
+  },
+
+  _onShutdownComplete: function () {
+    this._log.warn("Shutdown complete.");
+    this._shutdownComplete = true;
+
+    if (this._shutdownCompleteCallback) {
+      this._shutdownCompleteCallback();
+    }
+  },
+
+  _waitForShutdown: function () {
+    if (this._shutdownComplete) {
+      return;
+    }
+
+    this._shutdownCompleteCallback = Async.makeSpinningCallback();
+    this._shutdownCompleteCallback.wait();
+    this._shutdownCompleteCallback = null;
+  },
+
   /**
-   * Perform post-construction initialization and start background activity.
-   *
-   * If this isn't called, no data upload will occur.
+   * Convenience method to shut down the instance.
    *
-   * This returns a promise that will be fulfilled when all initialization
-   * activity is completed. It is not safe for this instance to perform
-   * additional actions until this promise has been resolved.
+   * This should *not* be called outside of tests.
    */
-  start: function start() {
-    let onExists = function onExists() {
-      this._policy.startPolling();
-      this._log.info("HealthReporter started.");
-
-      return Promise.resolve();
-    }.bind(this);
-
-    return this._ensureDirectoryExists(this._stateDir)
-               .then(onExists);
+  _shutdown: function () {
+    this._initiateShutdown();
+    this._waitForShutdown();
   },
 
   /**
-   * Stop background functionality.
+   * Return a promise that is resolved once the service has been initialized.
    */
-  stop: function stop() {
-    this._policy.stopPolling();
+  onInit: function () {
+    if (this._initializeHadError) {
+      throw new Error("Service failed to initialize.");
+    }
+
+    if (this._initialized) {
+      return Promise.resolve(this);
+    }
+
+    return this._initializedDeferred.promise;
   },
 
+  _performDailyMaintenance: function () {
+    this._log.info("Request to perform daily maintenance.");
+
+    if (!this._initialized) {
+      return;
+    }
+
+    let now = new Date();
+    let cutoff = new Date(now.getTime() - MILLISECONDS_PER_DAY * (DAYS_IN_PAYLOAD - 1));
+
+    // The operation is enqueued and put in a transaction by the storage module.
+    this._storage.pruneDataBefore(cutoff);
+  },
+
+  //--------------------
+  // Provider Management
+  //--------------------
+
   /**
-   * Register a `MetricsProvider` with this instance.
+   * Register a `Metrics.Provider` with this instance.
    *
    * This needs to be called or no data will be collected. See also
    * registerProvidersFromCategoryManager`.
    *
    * @param provider
-   *        (MetricsProvider) The provider to register for collection.
+   *        (Metrics.Provider) The provider to register for collection.
    */
-  registerProvider: function registerProvider(provider) {
+  registerProvider: function (provider) {
     return this._collector.registerProvider(provider);
   },
 
   /**
    * Registers providers from a category manager category.
    *
    * This examines the specified category entries and registers found
    * providers.
    *
    * Category entries are essentially JS modules and the name of the symbol
-   * within that module that is a `MetricsProvider` instance.
+   * within that module that is a `Metrics.Provider` instance.
    *
    * The category entry name is the name of the JS type for the provider. The
    * value is the resource:// URI to import which makes this type available.
    *
    * Example entry:
    *
    *   FooProvider resource://gre/modules/foo.jsm
    *
    * One can register entries in the application's .manifest file. e.g.
    *
    *   category healthreport-js-provider FooProvider resource://gre/modules/foo.jsm
    *
    * Then to load them:
    *
-   *   let reporter = new HealthReporter("healthreport.");
+   *   let reporter = getHealthReporter("healthreport.");
    *   reporter.registerProvidersFromCategoryManager("healthreport-js-provider");
    *
    * @param category
    *        (string) Name of category to query and load from.
    */
-  registerProvidersFromCategoryManager:
-    function registerProvidersFromCategoryManager(category) {
-
+  registerProvidersFromCategoryManager: function (category) {
+    this._log.info("Registering providers from category: " + category);
     let cm = Cc["@mozilla.org/categorymanager;1"]
                .getService(Ci.nsICategoryManager);
 
+    let promises = [];
     let enumerator = cm.enumerateCategory(category);
     while (enumerator.hasMoreElements()) {
       let entry = enumerator.getNext()
                             .QueryInterface(Ci.nsISupportsCString)
                             .toString();
 
       let uri = cm.getCategoryEntry(category, entry);
       this._log.info("Attempting to load provider from category manager: " +
                      entry + " from " + uri);
 
       try {
         let ns = {};
         Cu.import(uri, ns);
 
         let provider = new ns[entry]();
-        this.registerProvider(provider);
+        promises.push(this.registerProvider(provider));
       } catch (ex) {
         this._log.warn("Error registering provider from category manager: " +
                        entry + "; " + CommonUtils.exceptionStr(ex));
         continue;
       }
     }
+
+    return Task.spawn(function wait() {
+      for (let promise of promises) {
+        yield promise;
+      }
+    });
   },
 
   /**
    * Collect all measurements for all registered providers.
    */
-  collectMeasurements: function collectMeasurements() {
-    return this._collector.collectConstantMeasurements();
+  collectMeasurements: function () {
+    return this._collector.collectConstantData();
   },
 
   /**
    * Record the user's rejection of the data submission policy.
    *
    * This should be what everything uses to disable data submission.
    *
    * @param reason
    *        (string) Why data submission is being disabled.
    */
-  recordPolicyRejection: function recordPolicyRejection(reason) {
+  recordPolicyRejection: function (reason) {
     this._policy.recordUserRejection(reason);
   },
 
   /**
    * Record the user's acceptance of the data submission policy.
    *
    * This should be what everything uses to enable data submission.
    *
    * @param reason
    *        (string) Why data submission is being enabled.
    */
-  recordPolicyAcceptance: function recordPolicyAcceptance(reason) {
+  recordPolicyAcceptance: function (reason) {
     this._policy.recordUserAcceptance(reason);
   },
 
   /**
    * Whether the data submission policy has been accepted.
    *
    * If this is true, health data will be submitted unless one of the kill
    * switches is active.
@@ -301,44 +596,128 @@ HealthReporter.prototype = {
   /**
    * Request that server data be deleted.
    *
    * If deletion is scheduled to occur immediately, a promise will be returned
    * that will be fulfilled when the deletion attempt finishes. Otherwise,
    * callers should poll haveRemoteData() to determine when remote data is
    * deleted.
    */
-  requestDeleteRemoteData: function requestDeleteRemoteData(reason) {
+  requestDeleteRemoteData: function (reason) {
     if (!this.lastSubmitID) {
       return;
     }
 
     return this._policy.deleteRemoteData(reason);
   },
 
-  getJSONPayload: function getJSONPayload() {
+  getJSONPayload: function () {
+    return Task.spawn(this._getJSONPayload.bind(this, this._now()));
+  },
+
+  _getJSONPayload: function (now) {
+    let pingDateString = this._formatDate(now);
+    this._log.info("Producing JSON payload for " + pingDateString);
+
     let o = {
       version: 1,
-      thisPingDate: this._formatDate(this._now()),
-      providers: {},
+      thisPingDate: pingDateString,
+      data: {last: {}, days: {}},
     };
 
+    let outputDataDays = o.data.days;
+
+    // We need to be careful that data in errors does not leak potentially
+    // private information.
+    // FUTURE ask Privacy if we can put exception stacks in here.
+    let errors = [];
+
     let lastPingDate = this.lastPingDate;
     if (lastPingDate.getTime() > 0) {
       o.lastPingDate = this._formatDate(lastPingDate);
     }
 
-    for (let [name, provider] of this._collector.collectionResults) {
-      o.providers[name] = provider;
+    for (let provider of this._collector.providers) {
+      let providerName = provider.name;
+
+      let providerEntry = {
+        measurements: {},
+      };
+
+      for (let [measurementKey, measurement] of provider.measurements) {
+        let name = providerName + "." + measurement.name + "." + measurement.version;
+
+        let serializer;
+        try {
+          serializer = measurement.serializer(measurement.SERIALIZE_JSON);
+        } catch (ex) {
+          this._log.warn("Error obtaining serializer for measurement: " + name +
+                         ": " + CommonUtils.exceptionStr(ex));
+          errors.push("Could not obtain serializer: " + name);
+          continue;
+        }
+
+        let data;
+        try {
+          data = yield this._storage.getMeasurementValues(measurement.id);
+        } catch (ex) {
+          this._log.warn("Error obtaining data for measurement: " +
+                         name + ": " + CommonUtils.exceptionStr(ex));
+          errors.push("Could not obtain data: " + name);
+          continue;
+        }
+
+        if (data.singular.size) {
+          try {
+            o.data.last[name] = serializer.singular(data.singular);
+          } catch (ex) {
+            this._log.warn("Error serializing data: " + CommonUtils.exceptionStr(ex));
+            errors.push("Error serializing singular: " + name);
+            continue;
+          }
+        }
+
+        let dataDays = data.days;
+        for (let i = 0; i < DAYS_IN_PAYLOAD; i++) {
+          let date = new Date(now.getTime() - i * MILLISECONDS_PER_DAY);
+          if (!dataDays.hasDay(date)) {
+            continue;
+          }
+          let dateFormatted = this._formatDate(date);
+
+          try {
+            let serialized = serializer.daily(dataDays.getDay(date));
+            if (!serialized) {
+              continue;
+            }
+
+            if (!(dateFormatted in outputDataDays)) {
+              outputDataDays[dateFormatted] = {};
+            }
+
+            outputDataDays[dateFormatted][name] = serialized;
+          } catch (ex) {
+            this._log.warn("Error populating data for day: " +
+                           CommonUtils.exceptionStr(ex));
+            errors.push("Could not serialize day: " + name +
+                        " ( " + dateFormatted + ")");
+            continue;
+          }
+        }
+      }
     }
 
-    return JSON.stringify(o);
+    if (errors.length) {
+      o.errors = errors;
+    }
+
+    throw new Task.Result(JSON.stringify(o));
   },
 
-  _onBagheeraResult: function _onBagheeraResult(request, isDelete, result) {
+  _onBagheeraResult: function (request, isDelete, result) {
     this._log.debug("Received Bagheera result.");
 
     let promise = Promise.resolve(null);
 
     if (!result.transportSuccess) {
       request.onSubmissionFailureSoft("Network transport error.");
       return promise;
     }
@@ -357,46 +736,44 @@ HealthReporter.prototype = {
       this.lastPingDate = now;
     }
 
     request.onSubmissionSuccess(now);
 
     return promise;
   },
 
-  _onSubmitDataRequestFailure: function _onSubmitDataRequestFailure(error) {
+  _onSubmitDataRequestFailure: function (error) {
     this._log.error("Error processing request to submit data: " +
                     CommonUtils.exceptionStr(error));
   },
 
-  _formatDate: function _formatDate(date) {
+  _formatDate: function (date) {
     // Why, oh, why doesn't JS have a strftime() equivalent?
     return date.toISOString().substr(0, 10);
   },
 
 
-  _uploadData: function _uploadData(request) {
+  _uploadData: function (request) {
     let id = CommonUtils.generateUUID();
 
     this._log.info("Uploading data to server: " + this.serverURI + " " +
                    this.serverNamespace + ":" + id);
     let client = new BagheeraClient(this.serverURI);
 
-    let payload = this.getJSONPayload();
-
-    return this._saveLastPayload(payload)
-               .then(client.uploadJSON.bind(client,
-                                            this.serverNamespace,
-                                            id,
-                                            payload,
-                                            this.lastSubmitID))
-               .then(this._onBagheeraResult.bind(this, request, false));
+    return Task.spawn(function doUpload() {
+      let payload = yield this.getJSONPayload();
+      yield this._saveLastPayload(payload);
+      let result = yield client.uploadJSON(this.serverNamespace, id, payload,
+                                           this.lastSubmitID);
+      yield this._onBagheeraResult(request, false, result);
+    }.bind(this));
   },
 
-  _deleteRemoteData: function _deleteRemoteData(request) {
+  _deleteRemoteData: function (request) {
     if (!this.lastSubmitID) {
       this._log.info("Received request to delete remote data but no data stored.");
       request.onNoDataAvailable();
       return;
     }
 
     this._log.warn("Deleting remote data.");
     let client = new BagheeraClient(this.serverURI);
@@ -414,17 +791,17 @@ HealthReporter.prototype = {
     if (!profD || !profD.length) {
       throw new Error("Could not obtain profile directory. OS.File not " +
                       "initialized properly?");
     }
 
     return OS.Path.join(profD, "healthreport");
   },
 
-  _ensureDirectoryExists: function _ensureDirectoryExists(path) {
+  _ensureDirectoryExists: function (path) {
     let deferred = Promise.defer();
 
     OS.File.makeDir(path).then(
       function onResult() {
         deferred.resolve(true);
       },
       function onError(error) {
         if (error.becauseExists) {
@@ -438,17 +815,17 @@ HealthReporter.prototype = {
 
     return deferred.promise;
   },
 
   get _lastPayloadPath() {
     return OS.Path.join(this._stateDir, "lastpayload.json");
   },
 
-  _saveLastPayload: function _saveLastPayload(payload) {
+  _saveLastPayload: function (payload) {
     let path = this._lastPayloadPath;
     let pathTmp = path + ".tmp";
 
     let encoder = new TextEncoder();
     let buffer = encoder.encode(payload);
 
     return OS.File.writeAtomic(path, buffer, {tmpPath: pathTmp});
   },
@@ -457,17 +834,17 @@ HealthReporter.prototype = {
    * Obtain the last uploaded payload.
    *
    * The promise is resolved to a JSON-decoded object on success. The promise
    * is rejected if the last uploaded payload could not be found or there was
    * an error reading or parsing it.
    *
    * @return Promise<object>
    */
-  getLastPayload: function getLoadPayload() {
+  getLastPayload: function () {
     let path = this._lastPayloadPath;
 
     return OS.File.read(path).then(
       function onData(buffer) {
         let decoder = new TextDecoder();
         let json = JSON.parse(decoder.decode(buffer));
 
         return Promise.resolve(json);
@@ -481,31 +858,29 @@ HealthReporter.prototype = {
   _now: function _now() {
     return new Date();
   },
 
   //-----------------------------
   // HealthReportPolicy listeners
   //-----------------------------
 
-  onRequestDataUpload: function onRequestDataSubmission(request) {
+  onRequestDataUpload: function (request) {
     this.collectMeasurements()
         .then(this._uploadData.bind(this, request),
               this._onSubmitDataRequestFailure.bind(this));
   },
 
-  onNotifyDataPolicy: function onNotifyDataPolicy(request) {
+  onNotifyDataPolicy: function (request) {
     // This isn't very loosely coupled. We may want to have this call
     // registered listeners instead.
     Observers.notify("healthreport:notify-data-policy:request", request);
   },
 
-  onRequestRemoteDelete: function onRequestRemoteDelete(request) {
+  onRequestRemoteDelete: function (request) {
     this._deleteRemoteData(request);
   },
 
   //------------------------------------
   // End of HealthReportPolicy listeners
   //------------------------------------
-};
+});
 
-Object.freeze(HealthReporter.prototype);
-
new file mode 100644
--- /dev/null
+++ b/services/healthreport/modules-testing/utils.jsm
@@ -0,0 +1,259 @@
+/* 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 = [
+  "getAppInfo",
+  "updateAppInfo",
+  "makeFakeAppDir",
+  "createFakeCrash",
+  "InspectedHealthReporter",
+];
+
+
+const {classes: Cc, interfaces: Ci, results: Cr, utils: Cu} = Components;
+
+Cu.import("resource://gre/modules/commonjs/promise/core.js");
+Cu.import("resource://gre/modules/FileUtils.jsm");
+Cu.import("resource://gre/modules/osfile.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/services-common/utils.js");
+Cu.import("resource://gre/modules/services/healthreport/healthreporter.jsm");
+
+
+let APP_INFO = {
+  vendor: "Mozilla",
+  name: "xpcshell",
+  ID: "xpcshell@tests.mozilla.org",
+  version: "1",
+  appBuildID: "20121107",
+  platformVersion: "p-ver",
+  platformBuildID: "20121106",
+  inSafeMode: false,
+  logConsoleErrors: true,
+  OS: "XPCShell",
+  XPCOMABI: "noarch-spidermonkey",
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIXULAppInfo, Ci.nsIXULRuntime]),
+  invalidateCachesOnRestart: function() {},
+};
+
+
+/**
+ * Obtain a reference to the current object used to define XULAppInfo.
+ */
+this.getAppInfo = function () { return APP_INFO; }
+
+/**
+ * Update the current application info.
+ *
+ * If the argument is defined, it will be the object used. Else, APP_INFO is
+ * used.
+ *
+ * To change the current XULAppInfo, simply call this function. If there was
+ * a previously registered app info object, it will be unloaded and replaced.
+ */
+this.updateAppInfo = function (obj) {
+  obj = obj || APP_INFO;
+  APP_INFO = obj;
+
+  let id = Components.ID("{fbfae60b-64a4-44ef-a911-08ceb70b9f31}");
+  let cid = "@mozilla.org/xre/app-info;1";
+  let registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
+
+  // Unregister an existing factory if one exists.
+  try {
+    let existing = Components.manager.getClassObjectByContractID(cid, Ci.nsIFactory);
+    registrar.unregisterFactory(id, existing);
+  } catch (ex) {}
+
+  let factory = {
+    createInstance: function (outer, iid) {
+      if (outer != null) {
+        throw Cr.NS_ERROR_NO_AGGREGATION;
+      }
+
+      return obj.QueryInterface(iid);
+    },
+  };
+
+  registrar.registerFactory(id, "XULAppInfo", cid, factory);
+};
+
+// Reference needed in order for fake app dir provider to be active.
+let gFakeAppDirectoryProvider;
+
+/**
+ * Installs a fake UAppData directory.
+ *
+ * This is needed by tests because a UAppData directory typically isn't
+ * present in the test environment.
+ *
+ * This function is suitable for use in different components. If we ever
+ * establish a central location for convenient test helpers, this should
+ * go there.
+ *
+ * We create the new UAppData directory under the profile's directory
+ * because the profile directory is automatically cleaned as part of
+ * test shutdown.
+ *
+ * This returns a promise that will be resolved once the new directory
+ * is created and installed.
+ */
+this.makeFakeAppDir = function () {
+  let dirMode = OS.Constants.libc.S_IRWXU;
+  let dirService = Cc["@mozilla.org/file/directory_service;1"]
+                     .getService(Ci.nsIProperties);
+  let baseFile = dirService.get("ProfD", Ci.nsIFile);
+  let appD = baseFile.clone();
+  appD.append("UAppData");
+
+  if (gFakeAppDirectoryProvider) {
+    return Promise.resolve(appD.path);
+  }
+
+  function makeDir(f) {
+    if (f.exists()) {
+      return;
+    }
+
+    dump("Creating directory: " + f.path + "\n");
+    f.create(Ci.nsIFile.DIRECTORY_TYPE, dirMode);
+  }
+
+  makeDir(appD);
+
+  let reportsD = appD.clone();
+  reportsD.append("Crash Reports");
+
+  let pendingD = reportsD.clone();
+  pendingD.append("pending");
+  let submittedD = reportsD.clone();
+  submittedD.append("submitted");
+
+  makeDir(reportsD);
+  makeDir(pendingD);
+  makeDir(submittedD);
+
+  let provider = {
+    getFile: function (prop, persistent) {
+      persistent.value = true;
+      if (prop == "UAppData") {
+        return appD.clone();
+      }
+
+      throw Cr.NS_ERROR_FAILURE;
+    },
+
+    QueryInterace: function (iid) {
+      if (iid.equals(Ci.nsIDirectoryServiceProvider) ||
+          iid.equals(Ci.nsISupports)) {
+        return this;
+      }
+
+      throw Cr.NS_ERROR_NO_INTERFACE;
+    },
+  };
+
+  // Register the new provider.
+  dirService.QueryInterface(Ci.nsIDirectoryService)
+            .registerProvider(provider);
+
+  // And undefine the old one.
+  try {
+    dirService.undefine("UAppData");
+  } catch (ex) {};
+
+  gFakeAppDirectoryProvider = provider;
+
+  dump("Successfully installed fake UAppDir\n");
+  return Promise.resolve(appD.path);
+};
+
+
+/**
+ * Creates a fake crash in the Crash Reports directory.
+ *
+ * Currently, we just create a dummy file. A more robust implementation would
+ * create something that actually resembles a crash report file.
+ *
+ * This is very similar to code in crashreporter/tests/browser/head.js.
+ *
+ * FUTURE consolidate code in a shared JSM.
+ */
+this.createFakeCrash = function (submitted=false, date=new Date()) {
+  let id = CommonUtils.generateUUID();
+  let filename;
+
+  let paths = ["Crash Reports"];
+  let mode;
+
+  if (submitted) {
+    paths.push("submitted");
+    filename = "bp-" + id + ".txt";
+    mode = OS.Constants.libc.S_IRUSR | OS.Constants.libc.S_IWUSR |
+           OS.Constants.libc.S_IRGRP | OS.Constants.libc.S_IROTH;
+  } else {
+    paths.push("pending");
+    filename = id + ".dmp";
+    mode = OS.Constants.libc.S_IRUSR | OS.Constants.libc.S_IWUSR;
+  }
+
+  paths.push(filename);
+
+  let file = FileUtils.getFile("UAppData", paths, true);
+  file.create(file.NORMAL_FILE_TYPE, mode);
+  file.lastModifiedTime = date.getTime();
+  dump("Created fake crash: " + id + "\n");
+
+  return id;
+};
+
+
+/**
+ * A HealthReporter that is probed with various callbacks and counters.
+ *
+ * The purpose of this type is to aid testing of startup and shutdown.
+ */
+this.InspectedHealthReporter = function (branch) {
+  HealthReporter.call(this, branch);
+
+  this.onStorageCreated = null;
+  this.onCollectorInitialized = null;
+  this.collectorShutdownCount = 0;
+  this.storageCloseCount = 0;
+}
+
+InspectedHealthReporter.prototype = {
+  __proto__: HealthReporter.prototype,
+
+  _onStorageCreated: function (storage) {
+    if (this.onStorageCreated) {
+      this.onStorageCreated(storage);
+    }
+
+    return HealthReporter.prototype._onStorageCreated.call(this, storage);
+  },
+
+  _onCollectorInitialized: function () {
+    if (this.onCollectorInitialized) {
+      this.onCollectorInitialized();
+    }
+
+    return HealthReporter.prototype._onCollectorInitialized.call(this);
+  },
+
+  _onCollectorShutdown: function () {
+    this.collectorShutdownCount++;
+
+    return HealthReporter.prototype._onCollectorShutdown.call(this);
+  },
+
+  _onStorageClose: function () {
+    this.storageCloseCount++;
+
+    return HealthReporter.prototype._onStorageClose.call(this);
+  },
+};
+
--- a/services/healthreport/profile.jsm
+++ b/services/healthreport/profile.jsm
@@ -6,34 +6,36 @@
 
 this.EXPORTED_SYMBOLS = [
   "ProfileCreationTimeAccessor",
   "ProfileMetadataProvider",
 ];
 
 const {utils: Cu, classes: Cc, interfaces: Ci} = Components;
 
-const DEFAULT_PROFILE_MEASUREMENT_NAME = "org.mozilla.profile";
+const DEFAULT_PROFILE_MEASUREMENT_NAME = "age";
 const MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000;
 const REQUIRED_UINT32_TYPE = {type: "TYPE_UINT32"};
 
 Cu.import("resource://gre/modules/commonjs/promise/core.js");
+Cu.import("resource://gre/modules/Metrics.jsm");
 Cu.import("resource://gre/modules/osfile.jsm")
-Cu.import("resource://gre/modules/services/metrics/dataprovider.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
 Cu.import("resource://services-common/log4moz.js");
 Cu.import("resource://services-common/utils.js");
 
 // Profile creation time access.
 // This is separate from the provider to simplify testing and enable extraction
 // to a shared location in the future.
-function ProfileCreationTimeAccessor(profile) {
+function ProfileCreationTimeAccessor(profile, log) {
   this.profilePath = profile || OS.Constants.Path.profileDir;
   if (!this.profilePath) {
     throw new Error("No profile directory.");
   }
+  this._log = log || {"debug": function (s) { dump(s + "\n"); }};
 }
 ProfileCreationTimeAccessor.prototype = {
   /**
    * There are three ways we can get our creation time:
    *
    * 1. From our own saved value (to avoid redundant work).
    * 2. From the on-disk JSON file.
    * 3. By calculating it from the filesystem.
@@ -110,41 +112,44 @@ ProfileCreationTimeAccessor.prototype = 
                .then(onOldest.bind(this));
   },
 
   /**
    * Traverse the contents of the profile directory, finding the oldest file
    * and returning its creation timestamp.
    */
   getOldestProfileTimestamp: function () {
+    let self = this;
     let oldest = Date.now() + 1000;
     let iterator = new OS.File.DirectoryIterator(this.profilePath);
-dump("Iterating over profile " + this.profilePath);
+    self._log.debug("Iterating over profile " + this.profilePath);
     if (!iterator) {
       throw new Error("Unable to fetch oldest profile entry: no profile iterator.");
     }
 
     function onEntry(entry) {
-      if ("winLastWriteDate" in entry) {
-        // Under Windows, additional information allow us to sort files immediately
-        // without having to perform additional I/O.
-        let timestamp = entry.winCreationDate.getTime();
-        if (timestamp < oldest) {
-          oldest = timestamp;
+      function onStatSuccess(info) {
+        // OS.File doesn't seem to be behaving. See Bug 827148.
+        // Let's do the best we can. This whole function is defensive.
+        let date = info.winBirthDate || info.macBirthDate;
+        if (!date || !date.getTime()) {
+          // OS.File will only return file creation times of any kind on Mac
+          // and Windows, where birthTime is defined.
+          // That means we're unable to function on Linux, so we use mtime
+          // instead.
+          self._log.debug("No birth date. Using mtime.");
+          date = info.lastModificationDate;
         }
-        return;
-      }
 
-      // Under other OSes, we need to call OS.File.stat.
-      function onStatSuccess(info) {
-        let date = info.creationDate;
-        let timestamp = date.getTime();
-        dump("CREATION DATE: " + entry.path + " = " + date);
-        if (timestamp < oldest) {
-          oldest = timestamp;
+        if (date) {
+          let timestamp = date.getTime();
+          self._log.debug("Using date: " + entry.path + " = " + date);
+          if (timestamp < oldest) {
+            oldest = timestamp;
+          }
         }
       }
       return OS.File.stat(entry.path)
                     .then(onStatSuccess);
     }
 
     let promise = iterator.forEach(onEntry);
 
@@ -160,69 +165,66 @@ dump("Iterating over profile " + this.pr
 
     return promise.then(onSuccess, onFailure);
   },
 }
 
 /**
  * Measurements pertaining to the user's profile.
  */
-function ProfileMetadataMeasurement(name=DEFAULT_PROFILE_MEASUREMENT_NAME) {
-  MetricsMeasurement.call(this, name, 1);
+function ProfileMetadataMeasurement() {
+  Metrics.Measurement.call(this);
 }
 ProfileMetadataMeasurement.prototype = {
-  __proto__: MetricsMeasurement.prototype,
+  __proto__: Metrics.Measurement.prototype,
 
-  fields: {
+  name: DEFAULT_PROFILE_MEASUREMENT_NAME,
+  version: 1,
+
+  configureStorage: function () {
     // Profile creation date. Number of days since Unix epoch.
-    "profileCreation": REQUIRED_UINT32_TYPE,
+    return this.registerStorageField("profileCreation", this.storage.FIELD_LAST_NUMERIC);
   },
 };
 
 /**
  * Turn a millisecond timestamp into a day timestamp.
  *
  * @param msec a number of milliseconds since epoch.
  * @return the number of whole days denoted by the input.
  */
 function truncate(msec) {
   return Math.floor(msec / MILLISECONDS_PER_DAY);
 }
 
 /**
- * A MetricsProvider for profile metadata, such as profile creation time.
+ * A Metrics.Provider for profile metadata, such as profile creation time.
  */
-function ProfileMetadataProvider(name="ProfileMetadataProvider") {
-  MetricsProvider.call(this, name);
+function ProfileMetadataProvider() {
+  Metrics.Provider.call(this);
 }
 ProfileMetadataProvider.prototype = {
-  __proto__: MetricsProvider.prototype,
+  __proto__: Metrics.Provider.prototype,
+
+  name: "org.mozilla.profile",
+
+  measurementTypes: [ProfileMetadataMeasurement],
 
   getProfileCreationDays: function () {
-    let accessor = new ProfileCreationTimeAccessor();
+    let accessor = new ProfileCreationTimeAccessor(null, this._log);
 
     return accessor.created
                    .then(truncate);
   },
 
-  collectConstantMeasurements: function () {
-    let result = this.createResult();
-    result.expectMeasurement("org.mozilla.profile");
-    result.populate = this._populateConstants.bind(this);
-    return result;
-  },
+  collectConstantData: function () {
+    let m = this.getMeasurement(DEFAULT_PROFILE_MEASUREMENT_NAME, 1);
 
-  _populateConstants: function (result) {
-    let name = DEFAULT_PROFILE_MEASUREMENT_NAME;
-    result.addMeasurement(new ProfileMetadataMeasurement(name));
-    function onSuccess(days) {
-      result.setValue(name, "profileCreation", days);
-      result.finish();
-    }
-    function onFailure(ex) {
-      result.addError(ex);
-      result.finish();
-    }
-    return this.getProfileCreationDays()
-               .then(onSuccess, onFailure);
+    return Task.spawn(function collectConstant() {
+      let createdDays = yield this.getProfileCreationDays();
+
+      yield this.enqueueStorageOperation(function storeDays() {
+        return m.setLastNumeric("profileCreation", createdDays);
+      });
+    }.bind(this));
   },
 };
 
--- a/services/healthreport/providers.jsm
+++ b/services/healthreport/providers.jsm
@@ -10,103 +10,181 @@
  * currently have all the code in one file. When the overhead of
  * compartments reaches a reasonable level, this file should be split
  * up.
  */
 
 "use strict";
 
 this.EXPORTED_SYMBOLS = [
+  "AddonsProvider",
   "AppInfoProvider",
+  "CrashDirectoryService",
+  "CrashesProvider",
+  "SessionsProvider",
   "SysInfoProvider",
 ];
 
 const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
 
+Cu.import("resource://gre/modules/commonjs/promise/core.js");
+Cu.import("resource://gre/modules/osfile.jsm");
+Cu.import("resource://gre/modules/Metrics.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
-Cu.import("resource://gre/modules/services/metrics/dataprovider.jsm");
 Cu.import("resource://services-common/preferences.js");
 Cu.import("resource://services-common/utils.js");
 
-
-const REQUIRED_STRING_TYPE = {type: "TYPE_STRING"};
-const OPTIONAL_STRING_TYPE = {type: "TYPE_STRING", optional: true};
-const REQUIRED_UINT32_TYPE = {type: "TYPE_UINT32"};
-
+XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
+                                  "resource://gre/modules/AddonManager.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "UpdateChannel",
                                   "resource://gre/modules/UpdateChannel.jsm");
 
 /**
  * Represents basic application state.
  *
  * This is roughly a union of nsIXULAppInfo, nsIXULRuntime, with a few extra
  * pieces thrown in.
  */
 function AppInfoMeasurement() {
-  MetricsMeasurement.call(this, "appinfo", 1);
+  Metrics.Measurement.call(this);
 }
 
-AppInfoMeasurement.prototype = {
-  __proto__: MetricsMeasurement.prototype,
+AppInfoMeasurement.prototype = Object.freeze({
+  __proto__: Metrics.Measurement.prototype,
+
+  name: "appinfo",
+  version: 1,
+
+  LAST_TEXT_FIELDS: [
+    "vendor",
+    "name",
+    "id",
+    "version",
+    "appBuildID",
+    "platformVersion",
+    "platformBuildID",
+    "os",
+    "xpcomabi",
+    "updateChannel",
+    "distributionID",
+    "distributionVersion",
+    "hotfixVersion",
+    "locale",
+  ],
 
-  fields: {
-    vendor: REQUIRED_STRING_TYPE,
-    name: REQUIRED_STRING_TYPE,
-    id: REQUIRED_STRING_TYPE,
-    version: REQUIRED_STRING_TYPE,
-    appBuildID: REQUIRED_STRING_TYPE,
-    platformVersion: REQUIRED_STRING_TYPE,
-    platformBuildID: REQUIRED_STRING_TYPE,
-    os: REQUIRED_STRING_TYPE,
-    xpcomabi: REQUIRED_STRING_TYPE,
-    updateChannel: REQUIRED_STRING_TYPE,
-    distributionID: REQUIRED_STRING_TYPE,
-    distributionVersion: REQUIRED_STRING_TYPE,
-    hotfixVersion: REQUIRED_STRING_TYPE,
-    locale: REQUIRED_STRING_TYPE,
+  configureStorage: function () {
+    let self = this;
+    return Task.spawn(function configureStorage() {
+      for (let field of self.LAST_TEXT_FIELDS) {
+        yield self.registerStorageField(field, self.storage.FIELD_LAST_TEXT);
+      }
+
+      yield self.registerStorageField("isDefaultBrowser",
+                                      self.storage.FIELD_DAILY_LAST_NUMERIC);
+    });
   },
-};
+});
+
+
+function AppVersionMeasurement() {
+  Metrics.Measurement.call(this);
+}
+
+AppVersionMeasurement.prototype = Object.freeze({
+  __proto__: Metrics.Measurement.prototype,
 
-Object.freeze(AppInfoMeasurement.prototype);
+  name: "versions",
+  version: 1,
+
+  configureStorage: function () {
+    return this.registerStorageField("version",
+                                     this.storage.FIELD_DAILY_DISCRETE_TEXT);
+  },
+});
+
 
 
 this.AppInfoProvider = function AppInfoProvider() {
-  MetricsProvider.call(this, "app-info");
+  Metrics.Provider.call(this);
 
   this._prefs = new Preferences({defaultBranch: null});
 }
-AppInfoProvider.prototype = {
-  __proto__: MetricsProvider.prototype,
+AppInfoProvider.prototype = Object.freeze({
+  __proto__: Metrics.Provider.prototype,
+
+  name: "org.mozilla.appInfo",
+
+  measurementTypes: [AppInfoMeasurement, AppVersionMeasurement],
 
   appInfoFields: {
     // From nsIXULAppInfo.
     vendor: "vendor",
     name: "name",
     id: "ID",
     version: "version",
     appBuildID: "appBuildID",
     platformVersion: "platformVersion",
     platformBuildID: "platformBuildID",
 
     // From nsIXULRuntime.
     os: "OS",
     xpcomabi: "XPCOMABI",
   },
 
-  collectConstantMeasurements: function collectConstantMeasurements() {
-    let result = this.createResult();
-    result.expectMeasurement("appinfo");
-
-    result.populate = this._populateConstants.bind(this);
-    return result;
+  onInit: function () {
+    return Task.spawn(this._onInit.bind(this));
   },
 
-  _populateConstants: function _populateConstants(result) {
-    result.addMeasurement(new AppInfoMeasurement());
+  _onInit: function () {
+    // Services.appInfo should always be defined for any reasonably behaving
+    // Gecko app. If it isn't, we insert a empty string sentinel value.
+    let ai;
+    try {
+      ai = Services.appinfo;
+    } catch (ex) {
+      this._log.error("Could not obtain Services.appinfo: " +
+                     CommonUtils.exceptionStr(ex));
+      yield this._setCurrentVersion("");
+      return;
+    }
+
+    if (!ai) {
+      this._log.error("Services.appinfo is unavailable.");
+      yield this._setCurrentVersion("");
+      return;
+    }
+
+    let currentVersion = ai.version;
+    let lastVersion = yield this.getState("lastVersion");
+
+    if (currentVersion == lastVersion) {
+      return;
+    }
+
+    yield this._setCurrentVersion(currentVersion);
+  },
+
+  _setCurrentVersion: function (version) {
+    this._log.info("Recording new application version: " + version);
+    let m = this.getMeasurement("versions", 1);
+    m.addDailyDiscreteText("version", version);
+    return this.setState("lastVersion", version);
+  },
+
+  collectConstantData: function () {
+    return this.enqueueStorageOperation(function collect() {
+      return Task.spawn(this._populateConstants.bind(this));
+    }.bind(this));
+  },
+
+  _populateConstants: function () {
+    let m = this.getMeasurement(AppInfoMeasurement.prototype.name,
+                                AppInfoMeasurement.prototype.version);
 
     let ai;
     try {
       ai = Services.appinfo;
     } catch (ex) {
       this._log.warn("Could not obtain Services.appinfo: " +
                      CommonUtils.exceptionStr(ex));
       throw ex;
@@ -114,141 +192,818 @@ AppInfoProvider.prototype = {
 
     if (!ai) {
       this._log.warn("Services.appinfo is unavailable.");
       throw ex;
     }
 
     for (let [k, v] in Iterator(this.appInfoFields)) {
       try {
-        result.setValue("appinfo", k, ai[v]);
+        yield m.setLastText(k, ai[v]);
       } catch (ex) {
         this._log.warn("Error obtaining Services.appinfo." + v);
-        result.addError(ex);
       }
     }
 
     try {
-      result.setValue("appinfo", "updateChannel", UpdateChannel.get());
+      yield m.setLastText("updateChannel", UpdateChannel.get());
     } catch (ex) {
       this._log.warn("Could not obtain update channel: " +
                      CommonUtils.exceptionStr(ex));
-      result.addError(ex);
     }
 
-    result.setValue("appinfo", "distributionID", this._prefs.get("distribution.id", ""));
-    result.setValue("appinfo", "distributionVersion", this._prefs.get("distribution.version", ""));
-    result.setValue("appinfo", "hotfixVersion", this._prefs.get("extensions.hotfix.lastVersion", ""));
+    yield m.setLastText("distributionID", this._prefs.get("distribution.id", ""));
+    yield m.setLastText("distributionVersion", this._prefs.get("distribution.version", ""));
+    yield m.setLastText("hotfixVersion", this._prefs.get("extensions.hotfix.lastVersion", ""));
 
     try {
       let locale = Cc["@mozilla.org/chrome/chrome-registry;1"]
                      .getService(Ci.nsIXULChromeRegistry)
                      .getSelectedLocale("global");
-      result.setValue("appinfo", "locale", locale);
+      yield m.setLastText("locale", locale);
     } catch (ex) {
       this._log.warn("Could not obtain application locale: " +
                      CommonUtils.exceptionStr(ex));
-      result.addError(ex);
+    }
+
+    // FUTURE this should be retrieved periodically or at upload time.
+    yield this._recordDefaultBrowser(m);
+  },
+
+  _recordDefaultBrowser: function (m) {
+    let shellService;
+    try {
+      shellService = Cc["@mozilla.org/browser/shell-service;1"]
+                       .getService(Ci.nsIShellService);
+    } catch (ex) {
+      this._log.warn("Could not obtain shell service: " +
+                     CommonUtils.exceptionStr(ex));
     }
 
-    result.finish();
+    let isDefault = -1;
+
+    if (shellService) {
+      try {
+        // This uses the same set of flags used by the pref pane.
+        isDefault = shellService.isDefaultBrowser(false, true) ? 1 : 0;
+      } catch (ex) {
+        this._log.warn("Could not determine if default browser: " +
+                       CommonUtils.exceptionStr(ex));
+      }
+    }
+
+    return m.setDailyLastNumeric("isDefaultBrowser", isDefault);
   },
-};
-
-Object.freeze(AppInfoProvider.prototype);
+});
 
 
 function SysInfoMeasurement() {
-  MetricsMeasurement.call(this, "sysinfo", 1);
+  Metrics.Measurement.call(this);
 }
 
-SysInfoMeasurement.prototype = {
-  __proto__: MetricsMeasurement.prototype,
+SysInfoMeasurement.prototype = Object.freeze({
+  __proto__: Metrics.Measurement.prototype,
+
+  name: "sysinfo",
+  version: 1,
 
-  fields: {
-    cpuCount: REQUIRED_UINT32_TYPE,
-    memoryMB: REQUIRED_UINT32_TYPE,
-    manufacturer: OPTIONAL_STRING_TYPE,
-    device: OPTIONAL_STRING_TYPE,
-    hardware: OPTIONAL_STRING_TYPE,
-    name: OPTIONAL_STRING_TYPE,
-    version: OPTIONAL_STRING_TYPE,
-    architecture: OPTIONAL_STRING_TYPE,
+  configureStorage: function () {
+    return Task.spawn(function configureStorage() {
+      yield this.registerStorageField("cpuCount", this.storage.FIELD_LAST_NUMERIC);
+      yield this.registerStorageField("memoryMB", this.storage.FIELD_LAST_NUMERIC);
+      yield this.registerStorageField("manufacturer", this.storage.FIELD_LAST_TEXT);
+      yield this.registerStorageField("device", this.storage.FIELD_LAST_TEXT);
+      yield this.registerStorageField("hardware", this.storage.FIELD_LAST_TEXT);
+      yield this.registerStorageField("name", this.storage.FIELD_LAST_TEXT);
+      yield this.registerStorageField("version", this.storage.FIELD_LAST_TEXT);
+      yield this.registerStorageField("architecture", this.storage.FIELD_LAST_TEXT);
+    }.bind(this));
   },
-},
-
-Object.freeze(SysInfoMeasurement.prototype);
+});
 
 
 this.SysInfoProvider = function SysInfoProvider() {
-  MetricsProvider.call(this, "sys-info");
+  Metrics.Provider.call(this);
 };
 
-SysInfoProvider.prototype = {
-  __proto__: MetricsProvider.prototype,
+SysInfoProvider.prototype = Object.freeze({
+  __proto__: Metrics.Provider.prototype,
+
+  name: "org.mozilla.sysinfo",
+
+  measurementTypes: [SysInfoMeasurement],
 
   sysInfoFields: {
     cpucount: "cpuCount",
     memsize: "memoryMB",
     manufacturer: "manufacturer",
     device: "device",
     hardware: "hardware",
     name: "name",
     version: "version",
     arch: "architecture",
   },
 
-  INT_FIELDS: new Set("cpucount", "memsize"),
-
-  collectConstantMeasurements: function collectConstantMeasurements() {
-    let result = this.createResult();
-    result.expectMeasurement("sysinfo");
-
-    result.populate = this._populateConstants.bind(this);
-
-    return result;
+  collectConstantData: function () {
+    return this.enqueueStorageOperation(function collection() {
+      return Task.spawn(this._populateConstants.bind(this));
+    }.bind(this));
   },
 
-  _populateConstants: function _populateConstants(result) {
-    result.addMeasurement(new SysInfoMeasurement());
+  _populateConstants: function () {
+    let m = this.getMeasurement(SysInfoMeasurement.prototype.name,
+                                SysInfoMeasurement.prototype.version);
 
     let si = Cc["@mozilla.org/system-info;1"]
                .getService(Ci.nsIPropertyBag2);
 
     for (let [k, v] in Iterator(this.sysInfoFields)) {
       try {
         if (!si.hasKey(k)) {
           this._log.debug("Property not available: " + k);
           continue;
         }
 
         let value = si.getProperty(k);
+        let method = "setLastText";
 
-        if (this.INT_FIELDS.has(k)) {
+        if (["cpucount", "memsize"].indexOf(k) != -1) {
           let converted = parseInt(value, 10);
           if (Number.isNaN(converted)) {
-            result.addError(new Error("Value is not an integer: " + k + "=" +
-                                      value));
             continue;
           }
 
           value = converted;
+          method = "setLastNumeric";
         }
 
         // Round memory to mebibytes.
         if (k == "memsize") {
           value = Math.round(value / 1048576);
         }
 
-        result.setValue("sysinfo", v, value);
+        yield m[method](v, value);
       } catch (ex) {
         this._log.warn("Error obtaining system info field: " + k + " " +
                        CommonUtils.exceptionStr(ex));
-        result.addError(ex);
+      }
+    }
+  },
+});
+
+
+/**
+ * Holds information about the current/active session.
+ *
+ * The fields within the current session are moved to daily session fields when
+ * the application is shut down.
+ */
+function CurrentSessionMeasurement() {
+  Metrics.Measurement.call(this);
+}
+
+CurrentSessionMeasurement.prototype = Object.freeze({
+  __proto__: Metrics.Measurement.prototype,
+
+  name: "current",
+  version: 1,
+
+  LAST_NUMERIC_FIELDS: [
+    // Day on which the session was started.
+    // This is used to determine which day the record will be moved under when
+    // moved to daily sessions.
+    "startDay",
+
+    // Time in milliseconds the session was active for.
+    "activeTime",
+
+    // Total time in milliseconds of the session.
+    "totalTime",
+
+    // Startup times, in milliseconds.
+    "main",
+    "firstPaint",
+    "sessionRestored",
+  ],
+
+  configureStorage: function () {
+    return Task.spawn(function configureStorage() {
+      for (let field of this.LAST_NUMERIC_FIELDS) {
+        yield this.registerStorageField(field, this.storage.FIELD_LAST_NUMERIC);
+      }
+    }.bind(this));
+  },
+});
+
+
+/**
+ * Records a history of all application sessions.
+ */
+function PreviousSessionsMeasurement() {
+  Metrics.Measurement.call(this);
+}
+
+PreviousSessionsMeasurement.prototype = Object.freeze({
+  __proto__: Metrics.Measurement.prototype,
+
+  name: "previous",
+  version: 1,
+
+  DAILY_DISCRETE_NUMERIC_FIELDS: [
+    // Milliseconds of sessions that were properly shut down.
+    "cleanActiveTime",
+    "cleanTotalTime",
+
+    // Milliseconds of sessions that were not properly shut down.
+    "abortedActiveTime",
+    "abortedTotalTime",
+
+    // Startup times in milliseconds.
+    "main",
+    "firstPaint",
+    "sessionRestored",
+  ],
+
+  configureStorage: function () {
+    return Task.spawn(function configureStorage() {
+      for (let field of this.DAILY_DISCRETE_NUMERIC_FIELDS) {
+        yield this.registerStorageField(field, this.storage.FIELD_DAILY_DISCRETE_NUMERIC);
+      }
+    }.bind(this));
+  },
+});
+
+
+/**
+ * Records information about the current browser session.
+ *
+ * A browser session is defined as an application/process lifetime. We
+ * start a new session when the application starts (essentially when
+ * this provider is instantiated) and end the session on shutdown.
+ *
+ * As the application runs, we record basic information about the
+ * "activity" of the session. Activity is defined by the presence of
+ * physical input into the browser (key press, mouse click, touch, etc).
+ *
+ * We differentiate between regular sessions and "aborted" sessions. An
+ * aborted session is one that does not end expectedly. This is often the
+ * result of a crash. We detect aborted sessions by storing the current
+ * session separate from completed sessions. We normally move the
+ * current session to completed sessions on application shutdown. If a
+ * current session is present on application startup, that means that
+ * the previous session was aborted.
+ */
+this.SessionsProvider = function () {
+  Metrics.Provider.call(this);
+
+  this._startDate = null;
+  this._currentActiveTime = null;
+  this._lastActivityDate = null;
+  this._lastActivityWasInactive = false;
+};
+
+SessionsProvider.prototype = Object.freeze({
+  __proto__: Metrics.Provider.prototype,
+
+  name: "org.mozilla.appSessions",
+
+  measurementTypes: [CurrentSessionMeasurement, PreviousSessionsMeasurement],
+
+  _OBSERVERS: ["user-interaction-active", "user-interaction-inactive"],
+
+  onInit: function () {
+    return Task.spawn(this._onInit.bind(this));
+  },
+
+  _onInit: function () {
+    // We could cross day boundary between the application started and when
+    // this code is called. Meh.
+    let now = new Date();
+    this._startDate = now;
+    let current = this.getMeasurement("current", 1);
+
+    // Initialization occurs serially so we don't need to enqueue
+    // storage operations.
+    let currentData = yield this.storage.getMeasurementLastValuesFromMeasurementID(current.id);
+
+    // Normal shutdown should purge all data for this measurement. If
+    // there is data here, the session was aborted.
+    if (currentData.size) {
+      this._log.info("Data left over from old session. Counting as aborted.");
+      yield Task.spawn(this._moveCurrentToDaily.bind(this, currentData, true));
+    }
+
+    this._currentActiveTime = 0;
+    this._lastActivityDate = now;
+
+    this._log.debug("Registering new/current session.");
+    yield current.setLastNumeric("activeTime", 0, now);
+    yield current.setLastNumeric("totalTime", 0, now);
+    yield current.setLastNumeric("startDay", this._dateToDays(now), now);
+
+    let si = this._getStartupInfo();
+
+    for (let field of ["main", "firstPaint", "sessionRestored"]) {
+      if (!(field in si)) {
+        continue;
+      }
+
+      // si.process is the Date when the process actually started.
+      let value = si[field] - si.process;
+      yield current.setLastNumeric(field, value, now);
+    }
+
+    for (let channel of this._OBSERVERS) {
+      Services.obs.addObserver(this, channel, false);
+    }
+  },
+
+  onShutdown: function () {
+    for (let channel of this._OBSERVERS) {
+      Services.obs.removeObserver(this, channel);
+    }
+
+    return Task.spawn(this._onShutdown.bind(this));
+  },
+
+  _onShutdown: function () {
+    this._log.debug("Recording clean shutdown.");
+    yield this.recordBrowserActivity(true);
+    let current = this.getMeasurement("current", 1);
+
+    let self = this;
+    yield this.enqueueStorageOperation(function doShutdown() {
+      return Task.spawn(function shutdownTask() {
+        let data = yield self.storage.getMeasurementLastValuesFromMeasurementID(current.id);
+        yield self._moveCurrentToDaily(data, false);
+      });
+    });
+  },
+
+  /**
+   * Record browser activity.
+   *
+   * This should be called periodically to update the stored times of how often
+   * the user was active with the browser.
+   *
+   * The underlying browser activity observer fires every 5 seconds if there
+   * is activity. If there is inactivity, it fires after 5 seconds of inactivity
+   * and doesn't fire again until there is activity.
+   *
+   * @param active
+   *        (bool) Whether the browser was active or inactive.
+   */
+  recordBrowserActivity: function (active) {
+    // There is potential for clock skew to result in incorrect measurements
+    // here. We should count ticks instead of calculating intervals.
+    // FUTURE count ticks not intervals.
+    let now = new Date();
+    this._log.trace("Recording browser activity. Active? " + !!active);
+
+    let m = this.getMeasurement("current", 1);
+
+    let updateActive = active && !this._lastActivityWasInactive;
+    this._lastActivityWasInactive = !active;
+
+    if (updateActive) {
+      this._currentActiveTime += now - this._lastActivityDate;
+    }
+
+    this._lastActivityDate = now;
+
+    let totalTime = now - this._startDate;
+    let activeTime = this._currentActiveTime;
+
+    return this.enqueueStorageOperation(function op() {
+      let promise = m.setLastNumeric("totalTime", totalTime, now);
+
+      if (!updateActive) {
+        return promise;
+      }
+
+      return m.setLastNumeric("activeTime", activeTime, now);
+    });
+  },
+
+  _moveCurrentToDaily: function (fields, aborted) {
+    this._log.debug("Moving current session to past. Aborted? " + aborted);
+    let current = this.getMeasurement("current", 1);
+
+    function clearCurrent() {
+      current.deleteLastNumeric("startDay");
+      current.deleteLastNumeric("activeTime");
+      current.deleteLastNumeric("totalTime");
+      current.deleteLastNumeric("main");
+      current.deleteLastNumeric("firstPaint");
+      return current.deleteLastNumeric("sessionRestored");
+    }
+
+    // We should never have incomplete values. But if we do, handle it
+    // gracefully.
+    if (!fields.has("startDay") || !fields.has("activeTime") || !fields.has("totalTime")) {
+      yield clearCurrent();
+      return;
+    }
+
+    let daily = this.getMeasurement("previous", 1);
+
+    let startDays = fields.get("startDay")[1];
+    let activeTime = fields.get("activeTime")[1];
+    let totalTime = fields.get("totalTime")[1];
+
+    let date = this._daysToDate(startDays);
+    let type = aborted ? "aborted" : "clean";
+
+    yield daily.addDailyDiscreteNumeric(type + "ActiveTime", activeTime, date);
+    yield daily.addDailyDiscreteNumeric(type + "TotalTime", totalTime, date);
+
+    for (let field of ["main", "firstPaint", "sessionRestored"]) {
+      if (!fields.has(field)) {
+        this._log.info(field + " field not recorded for current session.");
+        continue;
+      }
+
+      yield daily.addDailyDiscreteNumeric(field, fields.get(field)[1], date);
+    }
+
+    yield clearCurrent();
+  },
+
+  observe: function (subject, topic, data) {
+    switch (topic) {
+      case "user-interaction-active":
+        this.recordBrowserActivity(true);
+        break;
+
+      case "user-interaction-inactive":
+        this.recordBrowserActivity(false);
+        break;
+    }
+  },
+
+  // Implemented as a function to allow for monkeypatching in tests.
+  _getStartupInfo: function () {
+    return Cc["@mozilla.org/toolkit/app-startup;1"]
+             .getService(Ci.nsIAppStartup)
+             .getStartupInfo();
+  },
+});
+
+/**
+ * Stores the set of active addons in storage.
+ *
+ * We do things a little differently than most other measurements. Because
+ * addons are difficult to shoehorn into distinct fields, we simply store a
+ * JSON blob in storage in a text field.
+ */
+function ActiveAddonsMeasurement() {
+  Metrics.Measurement.call(this);
+
+  this._serializers = {};
+  this._serializers[this.SERIALIZE_JSON] = {
+    singular: this._serializeJSONSingular.bind(this),
+    // We don't need a daily serializer because we have none of this data.
+  };
+}
+
+ActiveAddonsMeasurement.prototype = Object.freeze({
+  __proto__: Metrics.Measurement.prototype,
+
+  name: "active",
+  version: 1,
+
+  configureStorage: function () {
+    return this.registerStorageField("addons", this.storage.FIELD_LAST_TEXT);
+  },
+
+  _serializeJSONSingular: function (data) {
+    if (!data.has("addons")) {
+      this._log.warn("Don't have active addons info. Weird.");
+      return null;
+    }
+
+    // Exceptions are caught in the caller.
+    return JSON.parse(data.get("addons")[1]);
+  },
+});
+
+
+function AddonCountsMeasurement() {
+  Metrics.Measurement.call(this);
+}
+
+AddonCountsMeasurement.prototype = Object.freeze({
+  __proto__: Metrics.Measurement.prototype,
+
+  name: "counts",
+  version: 1,
+
+  configureStorage: function () {
+    return Task.spawn(function registerFields() {
+      yield this.registerStorageField("theme", this.storage.FIELD_DAILY_LAST_NUMERIC);
+      yield this.registerStorageField("lwtheme", this.storage.FIELD_DAILY_LAST_NUMERIC);
+      yield this.registerStorageField("plugin", this.storage.FIELD_DAILY_LAST_NUMERIC);
+      yield this.registerStorageField("extension", this.storage.FIELD_DAILY_LAST_NUMERIC);
+    }.bind(this));
+  },
+});
+
+
+this.AddonsProvider = function () {
+  Metrics.Provider.call(this);
+
+  this._prefs = new Preferences({defaultBranch: null});
+};
+
+AddonsProvider.prototype = Object.freeze({
+  __proto__: Metrics.Provider.prototype,
+
+  // Whenever these AddonListener callbacks are called, we repopulate
+  // and store the set of addons. Note that these events will only fire
+  // for restartless add-ons. For actions that require a restart, we
+  // will catch the change after restart. The alternative is a lot of
+  // state tracking here, which isn't desirable.
+  ADDON_LISTENER_CALLBACKS: [
+    "onEnabled",
+    "onDisabled",
+    "onInstalled",
+    "onUninstalled",
+  ],
+
+  name: "org.mozilla.addons",
+
+  measurementTypes: [
+    ActiveAddonsMeasurement,
+    AddonCountsMeasurement,
+  ],
+
+  onInit: function () {
+    let listener = {};
+
+    for (let method of this.ADDON_LISTENER_CALLBACKS) {
+      listener[method] = this._collectAndStoreAddons.bind(this);
+    }
+
+    this._listener = listener;
+    AddonManager.addAddonListener(this._listener);
+
+    return Promise.resolve();
+  },
+
+  onShutdown: function () {
+    AddonManager.removeAddonListener(this._listener);
+    this._listener = null;
+
+    return Promise.resolve();
+  },
+
+  collectConstantData: function () {
+    return this._collectAndStoreAddons();
+  },
+
+  _collectAndStoreAddons: function () {
+    let deferred = Promise.defer();
+
+    AddonManager.getAllAddons(function onAllAddons(addons) {
+      let data;
+      let addonsField;
+      try {
+        data = this._createDataStructure(addons);
+        addonsField = JSON.stringify(data.addons);
+      } catch (ex) {
+        this._log.warn("Exception when populating add-ons data structure: " +
+                       CommonUtils.exceptionStr(ex));
+        deferred.reject(ex);
+        return;
+      }
+
+      let now = new Date();
+      let active = this.getMeasurement("active", 1);
+      let counts = this.getMeasurement("counts", 1);
+
+      this.enqueueStorageOperation(function storageAddons() {
+        for (let type in data.counts) {
+          try {
+            counts.fieldID(type);
+          } catch (ex) {
+            this._log.warn("Add-on type without field: " + type);
+            continue;
+          }
+
+          counts.setDailyLastNumeric(type, data.counts[type], now);
+        }
+
+        return active.setLastText("addons", addonsField).then(
+          function onSuccess() { deferred.resolve(); },
+          function onError(error) { deferred.reject(error); }
+        );
+      }.bind(this));
+    }.bind(this));
+
+    return deferred.promise;
+  },
+
+  COPY_FIELDS: [
+    "userDisabled",
+    "appDisabled",
+    "version",
+    "type",
+    "scope",
+    "foreignInstall",
+    "hasBinaryComponents",
+  ],
+
+  _createDataStructure: function (addons) {
+    let data = {addons: {}, counts: {}};
+
+    for (let addon of addons) {
+      let optOutPref = "extensions." + addon.id + ".getAddons.cache.enabled";
+      if (!this._prefs.get(optOutPref, true)) {
+        this._log.debug("Ignoring add-on that's opted out of AMO updates: " +
+                        addon.id);
+        continue;
+      }
+
+      let obj = {};
+      for (let field of this.COPY_FIELDS) {
+        obj[field] = addon[field];
+      }
+
+      if (addon.installDate) {
+        obj.installDay = this._dateToDays(addon.installDate);
+      }
+
+      if (addon.updateDate) {
+        obj.updateDay = this._dateToDays(addon.updateDate);
+      }
+
+      data.addons[addon.id] = obj;
+
+      let type = addon.type;
+      data.counts[type] = (data.counts[type] || 0) + 1;
+    }
+
+    return data;
+  },
+});
+
+
+function DailyCrashesMeasurement() {
+  Metrics.Measurement.call(this);
+}
+
+DailyCrashesMeasurement.prototype = Object.freeze({
+  __proto__: Metrics.Measurement.prototype,
+
+  name: "crashes",
+  version: 1,
+
+  configureStorage: function () {
+    this.registerStorageField("pending", this.storage.FIELD_DAILY_COUNTER);
+    this.registerStorageField("submitted", this.storage.FIELD_DAILY_COUNTER);
+  },
+});
+
+this.CrashesProvider = function () {
+  Metrics.Provider.call(this);
+};
+
+CrashesProvider.prototype = Object.freeze({
+  __proto__: Metrics.Provider.prototype,
+
+  name: "org.mozilla.crashes",
+
+  measurementTypes: [DailyCrashesMeasurement],
+
+  collectConstantData: function () {
+    return Task.spawn(this._populateCrashCounts.bind(this));
+  },
+
+  _populateCrashCounts: function () {
+    let now = new Date();
+    let service = new CrashDirectoryService();
+
+    let pending = yield service.getPendingFiles();
+    let submitted = yield service.getSubmittedFiles();
+
+    let lastCheck = yield this.getState("lastCheck");
+    if (!lastCheck) {
+      lastCheck = 0;
+    } else {
+      lastCheck = parseInt(lastCheck, 10);
+      if (Number.isNaN(lastCheck)) {
+        lastCheck = 0;
       }
     }
 
-    result.finish();
+    let m = this.getMeasurement("crashes", 1);
+
+    // FUTURE detect mtimes in the future and react more intelligently.
+    for (let filename in pending) {
+      let modified = pending[filename].modified;
+
+      if (modified.getTime() < lastCheck) {
+        continue;
+      }
+
+      yield m.incrementDailyCounter("pending", modified);
+    }
+
+    for (let filename in submitted) {
+      let modified = submitted[filename].modified;
+
+      if (modified.getTime() < lastCheck) {
+        continue;
+      }
+
+      yield m.incrementDailyCounter("submitted", modified);
+    }
+
+    yield this.setState("lastCheck", "" + now.getTime());
   },
+});
+
+
+/**
+ * Helper for interacting with the crashes directory.
+ *
+ * FUTURE Extract to JSM alongside crashreporter. Use in about:crashes.
+ */
+this.CrashDirectoryService = function () {
+  let base = Cc["@mozilla.org/file/directory_service;1"]
+               .getService(Ci.nsIProperties)
+               .get("UAppData", Ci.nsIFile);
+
+  let cr = base.clone();
+  cr.append("Crash Reports");
+
+  let submitted = cr.clone();
+  submitted.append("submitted");
+
+  let pending = cr.clone();
+  pending.append("pending");
+
+  this._baseDir = base.path;
+  this._submittedDir = submitted.path;
+  this._pendingDir = pending.path;
 };
 
-Object.freeze(SysInfoProvider.prototype);
+CrashDirectoryService.prototype = Object.freeze({
+  RE_SUBMITTED_FILENAME: /^bp-.+\.txt$/,
+  RE_PENDING_FILENAME: /^.+\.dmp$/,
+
+  getPendingFiles: function () {
+    return this._getDirectoryEntries(this._pendingDir,
+                                     this.RE_PENDING_FILENAME);
+  },
+
+  getSubmittedFiles: function () {
+    return this._getDirectoryEntries(this._submittedDir,
+                                     this.RE_SUBMITTED_FILENAME);
+  },
+
+  _getDirectoryEntries: function (path, re) {
+    let files = {};
+
+    return Task.spawn(function iterateDirectory() {
+      // If the directory doesn't exist, exit immediately. Else, re-throw
+      // any errors.
+      try {
+        yield OS.File.stat(path);
+      } catch (ex if ex instanceof OS.File.Error) {
+        if (ex.becauseNoSuchFile) {
+          throw new Task.Result({});
+        }
+
+        throw ex;
+      }
 
+      let iterator = new OS.File.DirectoryIterator(path);
+
+      try {
+        while (true) {
+          let entry;
+          try {
+            entry = yield iterator.next();
+          } catch (ex if ex == StopIteration) {
+            break;
+          }
+
+          if (!entry.name.match(re)) {
+            continue;
+          }
+
+          let info = yield OS.File.stat(entry.path);
+          files[entry.name] = {
+            created: info.creationDate,
+            modified: info.lastModificationDate,
+            size: info.size,
+          };
+        }
+
+        throw new Task.Result(files);
+      } finally {
+        iterator.close();
+      }
+    });
+  },
+});
+
--- a/services/healthreport/tests/xpcshell/head.js
+++ b/services/healthreport/tests/xpcshell/head.js
@@ -9,8 +9,29 @@ do_get_profile();
 (function initMetricsTestingInfrastructure() {
   let ns = {};
   Components.utils.import("resource://testing-common/services-common/logging.js",
                           ns);
 
   ns.initTestLogging();
 }).call(this);
 
+(function createAppInfo() {
+  let ns = {};
+  Components.utils.import("resource://testing-common/services/healthreport/utils.jsm", ns);
+  ns.updateAppInfo();
+}).call(this);
+
+// The hack, it burns. This could go away if extensions code exposed its
+// test environment setup functions as a testing-only JSM. See similar
+// code in Sync's head_helpers.js.
+let gGlobalScope = this;
+function loadAddonManager() {
+  let ns = {};
+  Components.utils.import("resource://gre/modules/Services.jsm", ns);
+  let head = "../../../../toolkit/mozapps/extensions/test/xpcshell/head_addons.js";
+  let file = do_get_file(head);
+  let uri = ns.Services.io.newFileURI(file);
+  ns.Services.scriptloader.loadSubScript(uri.spec, gGlobalScope);
+  createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
+  startupManager();
+}
+
--- a/services/healthreport/tests/xpcshell/test_healthreporter.js
+++ b/services/healthreport/tests/xpcshell/test_healthreporter.js
@@ -5,281 +5,364 @@
 
 const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
 
 Cu.import("resource://services-common/observers.js");
 Cu.import("resource://services-common/preferences.js");
 Cu.import("resource://gre/modules/commonjs/promise/core.js");
 Cu.import("resource://gre/modules/services/healthreport/healthreporter.jsm");
 Cu.import("resource://gre/modules/services/healthreport/policy.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
 Cu.import("resource://testing-common/services-common/bagheeraserver.js");
 Cu.import("resource://testing-common/services/metrics/mocks.jsm");
+Cu.import("resource://testing-common/services/healthreport/utils.jsm");
 
 
 const SERVER_HOSTNAME = "localhost";
 const SERVER_PORT = 8080;
 const SERVER_URI = "http://" + SERVER_HOSTNAME + ":" + SERVER_PORT;
+const MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000;
 
 
 function defineNow(policy, now) {
   print("Adjusting fake system clock to " + now);
   Object.defineProperty(policy, "now", {
     value: function customNow() {
       return now;
     },
     writable: true,
   });
 }
 
-function getReporter(name, uri=SERVER_URI) {
+function getJustReporter(name, uri=SERVER_URI, inspected=false) {
   let branch = "healthreport.testing. " + name + ".";
 
   let prefs = new Preferences(branch);
   prefs.set("documentServerURI", uri);
+  prefs.set("dbName", name);
 
-  return new HealthReporter(branch);
+  let type = inspected ? InspectedHealthReporter : HealthReporter;
+  return new type(branch);
+}
+
+function getReporter(name, uri, inspected) {
+  let reporter = getJustReporter(name, uri, inspected);
+  return reporter.onInit();
 }
 
 function getReporterAndServer(name, namespace="test") {
-  let reporter = getReporter(name, SERVER_URI);
-  reporter.serverNamespace = namespace;
+  return Task.spawn(function get() {
+    let reporter = yield getReporter(name, SERVER_URI);
+    reporter.serverNamespace = namespace;
+
+    let server = new BagheeraServer(SERVER_URI);
+    server.createNamespace(namespace);
+
+    server.start(SERVER_PORT);
 
-  let server = new BagheeraServer(SERVER_URI);
-  server.createNamespace(namespace);
+    throw new Task.Result([reporter, server]);
+  });
+}
 
-  server.start(SERVER_PORT);
+function shutdownServer(server) {
+  let deferred = Promise.defer();
+  server.stop(deferred.resolve.bind(deferred));
 
-  return [reporter, server];
+  return deferred.promise;
 }
 
 function run_test() {
   run_next_test();
 }
 
-add_test(function test_constructor() {
-  let reporter = getReporter("constructor");
+add_task(function test_constructor() {
+  let reporter = yield getReporter("constructor");
 
   do_check_eq(reporter.lastPingDate.getTime(), 0);
   do_check_null(reporter.lastSubmitID);
 
   reporter.lastSubmitID = "foo";
   do_check_eq(reporter.lastSubmitID, "foo");
   reporter.lastSubmitID = null;
   do_check_null(reporter.lastSubmitID);
 
   let failed = false;
   try {
     new HealthReporter("foo.bar");
   } catch (ex) {
     failed = true;
-    do_check_true(ex.message.startsWith("Branch argument must end"));
+    do_check_true(ex.message.startsWith("Branch must end"));
   } finally {
     do_check_true(failed);
     failed = false;
   }
 
-  run_next_test();
+  reporter._shutdown();
+});
+
+add_task(function test_shutdown_normal() {
+  let reporter = yield getReporter("shutdown_normal");
+
+  // We can't send "quit-application" notification because the xpcshell runner
+  // will shut down!
+  reporter._initiateShutdown();
+  reporter._waitForShutdown();
 });
 
-add_test(function test_register_providers_from_category_manager() {
+add_task(function test_shutdown_storage_in_progress() {
+  let reporter = yield getJustReporter("shutdown_storage_in_progress", SERVER_URI, true);
+
+  reporter.onStorageCreated = function () {
+    print("Faking shutdown during storage initialization.");
+    reporter._initiateShutdown();
+  };
+
+  reporter._waitForShutdown();
+  do_check_eq(reporter.collectorShutdownCount, 0);
+  do_check_eq(reporter.storageCloseCount, 1);
+});
+
+// Ensure that a shutdown triggered while collector is initializing results in
+// shutdown and storage closure.
+add_task(function test_shutdown_collector_in_progress() {
+  let reporter = yield getJustReporter("shutdown_collect_in_progress", SERVER_URI, true);
+
+  reporter.onCollectorInitialized = function () {
+    print("Faking shutdown during collector initialization.");
+    reporter._initiateShutdown();
+  };
+
+  // This will hang if shutdown logic is busted.
+  reporter._waitForShutdown();
+  do_check_eq(reporter.collectorShutdownCount, 1);
+  do_check_eq(reporter.storageCloseCount, 1);
+});
+
+add_task(function test_register_providers_from_category_manager() {
   const category = "healthreporter-js-modules";
 
   let cm = Cc["@mozilla.org/categorymanager;1"]
              .getService(Ci.nsICategoryManager);
   cm.addCategoryEntry(category, "DummyProvider",
                       "resource://testing-common/services/metrics/mocks.jsm",
                       false, true);
 
-  let reporter = getReporter("category_manager");
-  do_check_eq(reporter._collector._providers.length, 0);
-  reporter.registerProvidersFromCategoryManager(category);
-  do_check_eq(reporter._collector._providers.length, 1);
+  let reporter = yield getReporter("category_manager");
+  do_check_eq(reporter._collector._providers.size, 0);
+  yield reporter.registerProvidersFromCategoryManager(category);
+  do_check_eq(reporter._collector._providers.size, 1);
 
-  run_next_test();
+  reporter._shutdown();
 });
 
-add_test(function test_start() {
-  let reporter = getReporter("start");
-  reporter.start().then(function onStarted() {
-    reporter.stop();
-    run_next_test();
-  });
-});
-
-add_test(function test_json_payload_simple() {
-  let reporter = getReporter("json_payload_simple");
+add_task(function test_json_payload_simple() {
+  let reporter = yield getReporter("json_payload_simple");
 
   let now = new Date();
-  let payload = reporter.getJSONPayload();
+  let payload = yield reporter.getJSONPayload();
   let original = JSON.parse(payload);
 
   do_check_eq(original.version, 1);
   do_check_eq(original.thisPingDate, reporter._formatDate(now));
-  do_check_eq(Object.keys(original.providers).length, 0);
+  do_check_eq(Object.keys(original.data.last).length, 0);
+  do_check_eq(Object.keys(original.data.days).length, 0);
 
   reporter.lastPingDate = new Date(now.getTime() - 24 * 60 * 60 * 1000 - 10);
 
-  original = JSON.parse(reporter.getJSONPayload());
+  original = JSON.parse(yield reporter.getJSONPayload());
   do_check_eq(original.lastPingDate, reporter._formatDate(reporter.lastPingDate));
 
   // This could fail if we cross UTC day boundaries at the exact instance the
   // test is executed. Let's tempt fate.
   do_check_eq(original.thisPingDate, reporter._formatDate(now));
 
-  run_next_test();
+  reporter._shutdown();
+});
+
+add_task(function test_json_payload_dummy_provider() {
+  let reporter = yield getReporter("json_payload_dummy_provider");
+
+  yield reporter.registerProvider(new DummyProvider());
+  yield reporter.collectMeasurements();
+  let payload = yield reporter.getJSONPayload();
+  print(payload);
+  let o = JSON.parse(payload);
+
+  do_check_eq(Object.keys(o.data.last).length, 1);
+  do_check_true("DummyProvider.DummyMeasurement.1" in o.data.last);
+
+  reporter._shutdown();
 });
 
-add_test(function test_json_payload_dummy_provider() {
-  let reporter = getReporter("json_payload_dummy_provider");
-
-  reporter.registerProvider(new DummyProvider());
-  reporter.collectMeasurements().then(function onResult() {
-    let o = JSON.parse(reporter.getJSONPayload());
+add_task(function test_json_payload_multiple_days() {
+  let reporter = yield getReporter("json_payload_multiple_days");
+  let provider = new DummyProvider();
+  yield reporter.registerProvider(provider);
 
-    do_check_eq(Object.keys(o.providers).length, 1);
-    do_check_true("DummyProvider" in o.providers);
-    do_check_true("measurements" in o.providers.DummyProvider);
-    do_check_true("DummyMeasurement" in o.providers.DummyProvider.measurements);
+  let now = new Date();
+  let m = provider.getMeasurement("DummyMeasurement", 1);
+  for (let i = 0; i < 200; i++) {
+    let date = new Date(now.getTime() - i * MILLISECONDS_PER_DAY);
+    yield m.incrementDailyCounter("daily-counter", date);
+    yield m.addDailyDiscreteNumeric("daily-discrete-numeric", i, date);
+    yield m.addDailyDiscreteNumeric("daily-discrete-numeric", i + 100, date);
+    yield m.addDailyDiscreteText("daily-discrete-text", "" + i, date);
+    yield m.addDailyDiscreteText("daily-discrete-text", "" + (i + 50), date);
+    yield m.setDailyLastNumeric("daily-last-numeric", date.getTime(), date);
+  }
 
-    run_next_test();
-  });
+  let payload = yield reporter.getJSONPayload();
+  print(payload);
+  let o = JSON.parse(payload);
+
+  do_check_eq(Object.keys(o.data.days).length, 180);
+  let today = reporter._formatDate(now);
+  do_check_true(today in o.data.days);
+
+  reporter._shutdown();
 });
 
-add_test(function test_notify_policy_observers() {
-  let reporter = getReporter("notify_policy_observers");
-
-  Observers.add("healthreport:notify-data-policy:request",
-                function onObserver(subject, data) {
-    Observers.remove("healthreport:notify-data-policy:request", onObserver);
+add_task(function test_idle_daily() {
+  let reporter = yield getReporter("idle_daily");
+  let provider = new DummyProvider();
+  yield reporter.registerProvider(provider);
 
-    do_check_true("foo" in subject);
+  let now = new Date();
+  let m = provider.getMeasurement("DummyMeasurement", 1);
+  for (let i = 0; i < 200; i++) {
+    let date = new Date(now.getTime() - i * MILLISECONDS_PER_DAY);
+    yield m.incrementDailyCounter("daily-counter", date);
+  }
 
-    run_next_test();
-  });
+  let values = yield m.getValues();
+  do_check_eq(values.days.size, 200);
+
+  Services.obs.notifyObservers(null, "idle-daily", null);
 
-  reporter.onNotifyDataPolicy({foo: "bar"});
+  let values = yield m.getValues();
+  do_check_eq(values.days.size, 180);
+
+  reporter._shutdown();
 });
 
-add_test(function test_data_submission_transport_failure() {
-  let reporter = getReporter("data_submission_transport_failure");
+add_task(function test_data_submission_transport_failure() {
+  let reporter = yield getReporter("data_submission_transport_failure");
   reporter.serverURI = "http://localhost:8080/";
   reporter.serverNamespace = "test00";
 
   let deferred = Promise.defer();
-  deferred.promise.then(function onResult(request) {
-    do_check_eq(request.state, request.SUBMISSION_FAILURE_SOFT);
-
-    run_next_test();
-  });
-
   let request = new DataSubmissionRequest(deferred, new Date(Date.now + 30000));
   reporter.onRequestDataUpload(request);
+
+  yield deferred.promise;
+  do_check_eq(request.state, request.SUBMISSION_FAILURE_SOFT);
+
+  reporter._shutdown();
 });
 
-add_test(function test_data_submission_success() {
-  let [reporter, server] = getReporterAndServer("data_submission_success");
+add_task(function test_data_submission_success() {
+  let [reporter, server] = yield getReporterAndServer("data_submission_success");
 
   do_check_eq(reporter.lastPingDate.getTime(), 0);
   do_check_false(reporter.haveRemoteData());
 
   let deferred = Promise.defer();
-  deferred.promise.then(function onResult(request) {
-    do_check_eq(request.state, request.SUBMISSION_SUCCESS);
-    do_check_neq(reporter.lastPingDate.getTime(), 0);
-    do_check_true(reporter.haveRemoteData());
-
-    server.stop(run_next_test);
-  });
 
   let request = new DataSubmissionRequest(deferred, new Date());
   reporter.onRequestDataUpload(request);
+  yield deferred.promise;
+  do_check_eq(request.state, request.SUBMISSION_SUCCESS);
+  do_check_true(reporter.lastPingDate.getTime() > 0);
+  do_check_true(reporter.haveRemoteData());
+
+  reporter._shutdown();
+  yield shutdownServer(server);
 });
 
-add_test(function test_recurring_daily_pings() {
-  let [reporter, server] = getReporterAndServer("recurring_daily_pings");
+add_task(function test_recurring_daily_pings() {
+  let [reporter, server] = yield getReporterAndServer("recurring_daily_pings");
   reporter.registerProvider(new DummyProvider());
 
   let policy = reporter._policy;
 
   defineNow(policy, policy._futureDate(-24 * 60 * 68 * 1000));
   policy.recordUserAcceptance();
   defineNow(policy, policy.nextDataSubmissionDate);
   let promise = policy.checkStateAndTrigger();
   do_check_neq(promise, null);
+  yield promise;
 
-  promise.then(function onUploadComplete() {
-    let lastID = reporter.lastSubmitID;
-
-    do_check_neq(lastID, null);
-    do_check_true(server.hasDocument(reporter.serverNamespace, lastID));
+  let lastID = reporter.lastSubmitID;
+  do_check_neq(lastID, null);
+  do_check_true(server.hasDocument(reporter.serverNamespace, lastID));
 
-    // Skip forward to next scheduled submission time.
-    defineNow(policy, policy.nextDataSubmissionDate);
-    let promise = policy.checkStateAndTrigger();
-    do_check_neq(promise, null);
-    promise.then(function onSecondUploadCOmplete() {
-      do_check_neq(reporter.lastSubmitID, lastID);
-      do_check_true(server.hasDocument(reporter.serverNamespace, reporter.lastSubmitID));
-      do_check_false(server.hasDocument(reporter.serverNamespace, lastID));
+  // Skip forward to next scheduled submission time.
+  defineNow(policy, policy.nextDataSubmissionDate);
+  promise = policy.checkStateAndTrigger();
+  do_check_neq(promise, null);
+  yield promise;
+  do_check_neq(reporter.lastSubmitID, lastID);
+  do_check_true(server.hasDocument(reporter.serverNamespace, reporter.lastSubmitID));
+  do_check_false(server.hasDocument(reporter.serverNamespace, lastID));
 
-      server.stop(run_next_test);
-    });
-  });
+  reporter._shutdown();
+  yield shutdownServer(server);
 });
 
-add_test(function test_request_remote_data_deletion() {
-  let [reporter, server] = getReporterAndServer("request_remote_data_deletion");
+add_task(function test_request_remote_data_deletion() {
+  let [reporter, server] = yield getReporterAndServer("request_remote_data_deletion");
 
   let policy = reporter._policy;
   defineNow(policy, policy._futureDate(-24 * 60 * 60 * 1000));
   policy.recordUserAcceptance();
   defineNow(policy, policy.nextDataSubmissionDate);
-  policy.checkStateAndTrigger().then(function onUploadComplete() {
-    let id = reporter.lastSubmitID;
-    do_check_neq(id, null);
-    do_check_true(server.hasDocument(reporter.serverNamespace, id));
+  yield policy.checkStateAndTrigger();
+  let id = reporter.lastSubmitID;
+  do_check_neq(id, null);
+  do_check_true(server.hasDocument(reporter.serverNamespace, id));
 
-    defineNow(policy, policy._futureDate(10 * 1000));
+  defineNow(policy, policy._futureDate(10 * 1000));
 
-    let promise = reporter.requestDeleteRemoteData();
-    do_check_neq(promise, null);
-    promise.then(function onDeleteComplete() {
-      do_check_null(reporter.lastSubmitID);
-      do_check_false(reporter.haveRemoteData());
-      do_check_false(server.hasDocument(reporter.serverNamespace, id));
+  let promise = reporter.requestDeleteRemoteData();
+  do_check_neq(promise, null);
+  yield promise;
+  do_check_null(reporter.lastSubmitID);
+  do_check_false(reporter.haveRemoteData());
+  do_check_false(server.hasDocument(reporter.serverNamespace, id));
 
-      server.stop(run_next_test);
-    });
-  });
+  reporter._shutdown();
+  yield shutdownServer(server);
 });
 
-add_test(function test_policy_accept_reject() {
-  let [reporter, server] = getReporterAndServer("policy_accept_reject");
+add_task(function test_policy_accept_reject() {
+  let [reporter, server] = yield getReporterAndServer("policy_accept_reject");
 
   do_check_false(reporter.dataSubmissionPolicyAccepted);
   do_check_false(reporter.willUploadData);
 
   reporter.recordPolicyAcceptance();
   do_check_true(reporter.dataSubmissionPolicyAccepted);
   do_check_true(reporter.willUploadData);
 
   reporter.recordPolicyRejection();
   do_check_false(reporter.dataSubmissionPolicyAccepted);
   do_check_false(reporter.willUploadData);
 
-  server.stop(run_next_test);
+  reporter._shutdown();
+  yield shutdownServer(server);
 });
 
 
-add_test(function test_upload_save_payload() {
-  let [reporter, server] = getReporterAndServer("upload_save_payload");
+add_task(function test_upload_save_payload() {
+  let [reporter, server] = yield getReporterAndServer("upload_save_payload");
 
   let deferred = Promise.defer();
   let request = new DataSubmissionRequest(deferred, new Date(), false);
 
-  reporter._uploadData(request).then(function onUpload() {
-    reporter.getLastPayload().then(function onJSON(json) {
-      do_check_true("thisPingDate" in json);
-      server.stop(run_next_test);
-    });
-  });
+  yield reporter._uploadData(request);
+  let json = yield reporter.getLastPayload();
+  do_check_true("thisPingDate" in json);
+
+  reporter._shutdown();
+  yield shutdownServer(server);
 });
 
--- a/services/healthreport/tests/xpcshell/test_profile.js
+++ b/services/healthreport/tests/xpcshell/test_profile.js
@@ -8,46 +8,39 @@ const {utils: Cu} = Components;
 const MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000;
 
 // Create profile directory before use.
 // It can be no older than a day ago….
 let profile_creation_lower = Date.now() - MILLISECONDS_PER_DAY;
 do_get_profile();
 
 Cu.import("resource://gre/modules/commonjs/promise/core.js");
-Cu.import("resource://gre/modules/services/metrics/dataprovider.jsm");
+Cu.import("resource://gre/modules/Metrics.jsm");
 Cu.import("resource://gre/modules/services/healthreport/profile.jsm");
 Cu.import("resource://gre/modules/Task.jsm");
 
+
 function MockProfileMetadataProvider(name="MockProfileMetadataProvider") {
-  ProfileMetadataProvider.call(this, name);
+  this.name = name;
+  ProfileMetadataProvider.call(this);
 }
 MockProfileMetadataProvider.prototype = {
   __proto__: ProfileMetadataProvider.prototype,
 
   getProfileCreationDays: function getProfileCreationDays() {
     return Promise.resolve(1234);
   },
 };
 
 
 function run_test() {
   run_next_test();
 }
 
 /**
- * Treat the provided function as a generator of promises,
- * suitable for use with Task.spawn. Success runs next test;
- * failure throws.
- */
-function testTask(promiseFunction) {
-  Task.spawn(promiseFunction).then(run_next_test, do_throw);
-}
-
-/**
  * Ensure that OS.File works in our environment.
  * This test can go once there are xpcshell tests for OS.File.
  */
 add_test(function use_os_file() {
   Cu.import("resource://gre/modules/osfile.jsm")
 
   // Ensure that we get constants, too.
   do_check_neq(OS.Constants.Path.profileDir, null);
@@ -79,77 +72,62 @@ add_test(function test_time_accessor_no_
      .then(function onSuccess(json) {
        do_throw("File existed!");
      },
      function onFailure() {
        run_next_test();
      });
 });
 
-add_test(function test_time_accessor_named_file() {
+add_task(function test_time_accessor_named_file() {
   let acc = getAccessor();
 
-  testTask(function () {
-    // There should be no file yet.
-    yield acc.writeTimes({created: 12345}, "test.json");
-    yield acc.readTimes("test.json")
-             .then(function onSuccess(json) {
-               print("Read: " + JSON.stringify(json));
-               do_check_eq(12345, json.created);
-               run_next_test();
-             });
-  });
+  // There should be no file yet.
+  yield acc.writeTimes({created: 12345}, "test.json");
+  let json = yield acc.readTimes("test.json")
+  print("Read: " + JSON.stringify(json));
+  do_check_eq(12345, json.created);
 });
 
-add_test(function test_time_accessor_creates_file() {
+add_task(function test_time_accessor_creates_file() {
   let lower = profile_creation_lower;
 
   // Ensure that provided contents are merged, and existing
   // files can be overwritten. These two things occur if we
   // read and then decide that we have to write.
   let acc = getAccessor();
   let existing = {abc: "123", easy: "abc"};
   let expected;
 
-  testTask(function () {
-    yield acc.computeAndPersistTimes(existing, "test2.json")
-             .then(function onSuccess(created) {
-               let upper = Date.now() + 1000;
-               print(lower + " < " + created + " <= " + upper);
-               do_check_true(lower < created);
-               do_check_true(upper >= created);
-               expected = created;
-             });
-    yield acc.readTimes("test2.json")
-             .then(function onSuccess(json) {
-               print("Read: " + JSON.stringify(json));
-               do_check_eq("123", json.abc);
-               do_check_eq("abc", json.easy);
-               do_check_eq(expected, json.created);
-             });
-  });
+  let created = yield acc.computeAndPersistTimes(existing, "test2.json")
+  let upper = Date.now() + 1000;
+  print(lower + " < " + created + " <= " + upper);
+  do_check_true(lower < created);
+  do_check_true(upper >= created);
+  expected = created;
+
+  let json = yield acc.readTimes("test2.json")
+  print("Read: " + JSON.stringify(json));
+  do_check_eq("123", json.abc);
+  do_check_eq("abc", json.easy);
+  do_check_eq(expected, json.created);
 });
 
-add_test(function test_time_accessor_all() {
+add_task(function test_time_accessor_all() {
   let lower = profile_creation_lower;
   let acc = getAccessor();
   let expected;
-  testTask(function () {
-    yield acc.created
-             .then(function onSuccess(created) {
-               let upper = Date.now() + 1000;
-               do_check_true(lower < created);
-               do_check_true(upper >= created);
-               expected = created;
-             });
-    yield acc.created
-             .then(function onSuccess(again) {
-               do_check_eq(expected, again);
-             });
-  });
+  let created = yield acc.created
+  let upper = Date.now() + 1000;
+  do_check_true(lower < created);
+  do_check_true(upper >= created);
+  expected = created;
+
+  let again = yield acc.created
+  do_check_eq(expected, again);
 });
 
 add_test(function test_constructor() {
   let provider = new ProfileMetadataProvider("named");
   run_next_test();
 });
 
 add_test(function test_profile_files() {
@@ -166,48 +144,53 @@ add_test(function test_profile_files() {
     do_throw("Directory iteration failed: " + ex);
   }
 
   provider.getProfileCreationDays().then(onSuccess, onFailure);
 });
 
 // A generic test helper. We use this with both real
 // and mock providers in these tests.
-function test_collect_constant(provider, valueTest) {
-  let result = provider.collectConstantMeasurements();
-  do_check_true(result instanceof MetricsCollectionResult);
+function test_collect_constant(provider) {
+  return Task.spawn(function () {
+    yield provider.collectConstantData();
 
-  result.onFinished(function onFinished() {
-    do_check_eq(result.expectedMeasurements.size, 1);
-    do_check_true(result.expectedMeasurements.has("org.mozilla.profile"));
-    let m = result.measurements.get("org.mozilla.profile");
-    do_check_true(!!m);
-    valueTest(m.getValue("profileCreation"));
+    let m = provider.getMeasurement("age", 1);
+    do_check_neq(m, null);
+    let values = yield m.getValues();
+    do_check_eq(values.singular.size, 1);
+    do_check_true(values.singular.has("profileCreation"));
 
-    run_next_test();
+    throw new Task.Result(values.singular.get("profileCreation")[1]);
   });
-
-  result.populate(result);
 }
 
-add_test(function test_collect_constant_mock() {
+add_task(function test_collect_constant_mock() {
+  let storage = yield Metrics.Storage("collect_constant_mock");
   let provider = new MockProfileMetadataProvider();
-  function valueTest(v) {
-    do_check_eq(v, 1234);
-  }
-  test_collect_constant(provider, valueTest);
+  yield provider.init(storage);
+
+  let v = yield test_collect_constant(provider);
+  do_check_eq(v, 1234);
+
+  yield storage.close();
 });
 
-add_test(function test_collect_constant_real() {
+add_task(function test_collect_constant_real() {
   let provider = new ProfileMetadataProvider();
-  function valueTest(v) {
-    let ms = v * MILLISECONDS_PER_DAY;
-    let lower = profile_creation_lower;
-    let upper = Date.now() + 1000;
-    print("Day:   " + v);
-    print("msec:  " + ms);
-    print("Lower: " + lower);
-    print("Upper: " + upper);
-    do_check_true(lower <= ms);
-    do_check_true(upper >= ms);
-  }
-  test_collect_constant(provider, valueTest);
+  let storage = yield Metrics.Storage("collect_constant_real");
+  yield provider.init(storage);
+
+  let v = yield test_collect_constant(provider);
+
+  let ms = v * MILLISECONDS_PER_DAY;
+  let lower = profile_creation_lower;
+  let upper = Date.now() + 1000;
+  print("Day:   " + v);
+  print("msec:  " + ms);
+  print("Lower: " + lower);
+  print("Upper: " + upper);
+  do_check_true(lower <= ms);
+  do_check_true(upper >= ms);
+
+  yield storage.close();
 });
+
new file mode 100644
--- /dev/null
+++ b/services/healthreport/tests/xpcshell/test_provider_addons.js
@@ -0,0 +1,119 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const {utils: Cu} = Components;
+
+
+Cu.import("resource://gre/modules/Metrics.jsm");
+Cu.import("resource://gre/modules/services/healthreport/providers.jsm");
+
+
+function run_test() {
+  loadAddonManager();
+  run_next_test();
+}
+
+add_test(function test_constructor() {
+  let provider = new AddonsProvider();
+
+  run_next_test();
+});
+
+add_task(function test_init() {
+  let storage = yield Metrics.Storage("init");
+  let provider = new AddonsProvider();
+  yield provider.init(storage);
+  yield provider.shutdown();
+
+  yield storage.close();
+});
+
+function monkeypatchAddons(provider, addons) {
+  if (!Array.isArray(addons)) {
+    throw new Error("Must define array of addon objects.");
+  }
+
+  Object.defineProperty(provider, "_createDataStructure", {
+    value: function _createDataStructure() {
+      return AddonsProvider.prototype._createDataStructure.call(provider, addons);
+    },
+  });
+}
+
+add_task(function test_collect() {
+  let storage = yield Metrics.Storage("collect");
+  let provider = new AddonsProvider();
+  yield provider.init(storage);
+
+  let now = new Date();
+
+  // FUTURE install add-on via AddonManager and don't use monkeypatching.
+  let addons = [
+    {
+      id: "addon0",
+      userDisabled: false,
+      appDisabled: false,
+      version: "1",
+      type: "extension",
+      scope: 1,
+      foreignInstall: false,
+      hasBinaryComponents: false,
+      installDate: now,
+      updateDate: now,
+    },
+    {
+      id: "addon1",
+      userDisabled: false,
+      appDisabled: false,
+      version: "2",
+      type: "plugin",
+      scope: 1,
+      foreignInstall: false,
+      hasBinaryComponents: false,
+      installDate: now,
+      updateDate: now,
+    },
+  ];
+
+  monkeypatchAddons(provider, addons);
+
+  yield provider.collectConstantData();
+
+  let active = provider.getMeasurement("active", 1);
+  let data = yield active.getValues();
+
+  do_check_eq(data.days.size, 0);
+  do_check_eq(data.singular.size, 1);
+  do_check_true(data.singular.has("addons"));
+
+  let json = data.singular.get("addons")[1];
+  let value = JSON.parse(json);
+  do_check_eq(typeof(value), "object");
+  do_check_eq(Object.keys(value).length, 2);
+  do_check_true("addon0" in value);
+  do_check_true("addon1" in value);
+
+  let serializer = active.serializer(active.SERIALIZE_JSON);
+  let serialized = serializer.singular(data.singular);
+  do_check_eq(typeof(serialized), "object");
+  do_check_eq(Object.keys(serialized).length, 2);
+  do_check_true("addon0" in serialized);
+  do_check_true("addon1" in serialized);
+
+  let counts = provider.getMeasurement("counts", 1);
+  data = yield counts.getValues();
+  do_check_eq(data.days.size, 1);
+  do_check_eq(data.singular.size, 0);
+  do_check_true(data.days.hasDay(now));
+
+  value = data.days.getDay(now);
+  do_check_eq(value.size, 2);
+  do_check_eq(value.get("extension"), 1);
+  do_check_eq(value.get("plugin"), 1);
+
+  yield provider.shutdown();
+  yield storage.close();
+});
+
--- a/services/healthreport/tests/xpcshell/test_provider_appinfo.js
+++ b/services/healthreport/tests/xpcshell/test_provider_appinfo.js
@@ -1,81 +1,113 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 const {interfaces: Ci, results: Cr, utils: Cu} = Components;
 
+Cu.import("resource://gre/modules/Metrics.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/services/healthreport/providers.jsm");
-Cu.import("resource://gre/modules/services/metrics/dataprovider.jsm");
+Cu.import("resource://testing-common/services/healthreport/utils.jsm");
+
 
 function run_test() {
-  let appInfo = {
-    vendor: "Mozilla",
-    name: "xpcshell",
-    ID: "xpcshell@tests.mozilla.org",
-    version: "1",
-    appBuildID: "20121107",
-    platformVersion: "p-ver",
-    platformBuildID: "20121106",
-    inSafeMode: false,
-    logConsoleErrors: true,
-    OS: "XPCShell",
-    XPCOMABI: "noarch-spidermonkey",
-    QueryInterface: XPCOMUtils.generateQI([Ci.nsIXULAppInfo, Ci.nsIXULRuntime]),
-    invalidateCachesOnRestart: function() {},
-  };
-
-  let factory = {
-    createInstance: function createInstance(outer, iid) {
-      if (outer != null) {
-        throw Cr.NS_ERROR_NO_AGGREGATION;
-      }
-
-      return appInfo.QueryInterface(iid);
-    },
-  };
-
-  let registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
-  registrar.registerFactory(Components.ID("{fbfae60b-64a4-44ef-a911-08ceb70b9f31}"),
-                            "XULAppInfo", "@mozilla.org/xre/app-info;1",
-                            factory);
-
   run_next_test();
 }
 
 add_test(function test_constructor() {
   let provider = new AppInfoProvider();
 
   run_next_test();
 });
 
-add_test(function test_collect_smoketest() {
+add_task(function test_collect_smoketest() {
+  let storage = yield Metrics.Storage("collect_smoketest");
   let provider = new AppInfoProvider();
+  yield provider.init(storage);
 
-  let result = provider.collectConstantMeasurements();
-  do_check_true(result instanceof MetricsCollectionResult);
+  let now = new Date();
+  yield provider.collectConstantData();
+
+  let m = provider.getMeasurement("appinfo", 1);
+  let data = yield storage.getMeasurementValues(m.id);
+  let serializer = m.serializer(m.SERIALIZE_JSON);
+  let d = serializer.singular(data.singular);
 
-  result.onFinished(function onFinished() {
-    do_check_eq(result.expectedMeasurements.size, 1);
-    do_check_true(result.expectedMeasurements.has("appinfo"));
-    do_check_eq(result.measurements.size, 1);
-    do_check_true(result.measurements.has("appinfo"));
-    do_check_eq(result.errors.length, 0);
+  do_check_eq(d.vendor, "Mozilla");
+  do_check_eq(d.name, "xpcshell");
+  do_check_eq(d.id, "xpcshell@tests.mozilla.org");
+  do_check_eq(d.version, "1");
+  do_check_eq(d.appBuildID, "20121107");
+  do_check_eq(d.platformVersion, "p-ver");
+  do_check_eq(d.platformBuildID, "20121106");
+  do_check_eq(d.os, "XPCShell");
+  do_check_eq(d.xpcomabi, "noarch-spidermonkey");
+
+  do_check_eq(data.days.size, 1);
+  do_check_true(data.days.hasDay(now));
+  let day = data.days.getDay(now);
+  do_check_eq(day.size, 1);
+  do_check_true(day.has("isDefaultBrowser"));
+
+  // TODO Bug 827189 Actually test this properly. On some local builds, this
+  // is always -1 (the service throws). On buildbot, it seems to always be 0.
+  do_check_neq(day.get("isDefaultBrowser"), 1);
+
+  yield provider.shutdown();
+  yield storage.close();
+});
+
+add_task(function test_record_version() {
+  let storage = yield Metrics.Storage("record_version");
+
+  let provider = new AppInfoProvider();
+  let now = new Date();
+  yield provider.init(storage);
 
-    let ai = result.measurements.get("appinfo");
-    do_check_eq(ai.getValue("vendor"), "Mozilla");
-    do_check_eq(ai.getValue("name"), "xpcshell");
-    do_check_eq(ai.getValue("id"), "xpcshell@tests.mozilla.org");
-    do_check_eq(ai.getValue("version"), "1");
-    do_check_eq(ai.getValue("appBuildID"), "20121107");
-    do_check_eq(ai.getValue("platformVersion"), "p-ver");
-    do_check_eq(ai.getValue("platformBuildID"), "20121106");
-    do_check_eq(ai.getValue("os"), "XPCShell");
-    do_check_eq(ai.getValue("xpcomabi"), "noarch-spidermonkey");
+  // The provider records information on startup.
+  let m = provider.getMeasurement("versions", 1);
+  let data = yield m.getValues();
+
+  do_check_true(data.days.hasDay(now));
+  let day = data.days.getDay(now);
+  do_check_eq(day.size, 1);
+  do_check_true(day.has("version"));
+  let value = day.get("version");
+  do_check_true(Array.isArray(value));
+  do_check_eq(value.length, 1);
+  let ai = getAppInfo();
+  do_check_eq(value, ai.version);
+
+  yield provider.shutdown();
+  yield storage.close();
+});
+
+add_task(function test_record_version_change() {
+  let storage = yield Metrics.Storage("record_version_change");
 
-    run_next_test();
-  });
+  let provider = new AppInfoProvider();
+  let now = new Date();
+  yield provider.init(storage);
+  yield provider.shutdown();
+
+  let ai = getAppInfo();
+  ai.version = "2";
+  updateAppInfo(ai);
+
+  provider = new AppInfoProvider();
+  yield provider.init(storage);
 
-  result.populate(result);
+  // There should be 2 records in the versions history.
+  let m = provider.getMeasurement("versions", 1);
+  let data = yield m.getValues();
+  do_check_true(data.days.hasDay(now));
+  let day = data.days.getDay(now);
+  let value = day.get("version");
+  do_check_true(Array.isArray(value));
+  do_check_eq(value.length, 2);
+  do_check_eq(value[1], "2");
+
+  yield provider.shutdown();
+  yield storage.close();
 });
new file mode 100644
--- /dev/null
+++ b/services/healthreport/tests/xpcshell/test_provider_crashes.js
@@ -0,0 +1,146 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const {utils: Cu} = Components;
+
+
+Cu.import("resource://gre/modules/Metrics.jsm");
+Cu.import("resource://gre/modules/services/healthreport/providers.jsm");
+Cu.import("resource://testing-common/services/healthreport/utils.jsm");
+
+
+const MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000;
+
+
+function run_test() {
+  makeFakeAppDir().then(run_next_test, do_throw);
+}
+
+let gPending = {};
+let gSubmitted = {};
+
+add_task(function test_directory_service() {
+  let d = new CrashDirectoryService();
+
+  let entries = yield d.getPendingFiles();
+  do_check_eq(typeof(entries), "object");
+  do_check_eq(Object.keys(entries).length, 0);
+
+  entries = yield d.getSubmittedFiles();
+  do_check_eq(typeof(entries), "object");
+  do_check_eq(Object.keys(entries).length, 0);
+
+  let now = new Date();
+
+  // We lose granularity when writing to filesystem.
+  now.setUTCMilliseconds(0);
+  let dates = [];
+  for (let i = 0; i < 10; i++) {
+    dates.push(new Date(now.getTime() - i * MILLISECONDS_PER_DAY));
+  }
+
+  let pending = {};
+  let submitted = {};
+  for (let date of dates) {
+    pending[createFakeCrash(false, date)] = date;
+    submitted[createFakeCrash(true, date)] = date;
+  }
+
+  entries = yield d.getPendingFiles();
+  do_check_eq(Object.keys(entries).length, Object.keys(pending).length);
+  for (let id in pending) {
+    let filename = id + ".dmp";
+    do_check_true(filename in entries);
+    do_check_eq(entries[filename].modified.getTime(), pending[id].getTime());
+  }
+
+  entries = yield d.getSubmittedFiles();
+  do_check_eq(Object.keys(entries).length, Object.keys(submitted).length);
+  for (let id in submitted) {
+    let filename = "bp-" + id + ".txt";
+    do_check_true(filename in entries);
+    do_check_eq(entries[filename].modified.getTime(), submitted[id].getTime());
+  }
+
+  gPending = pending;
+  gSubmitted = submitted;
+});
+
+add_test(function test_constructor() {
+  let provider = new CrashesProvider();
+
+  run_next_test();
+});
+
+add_task(function test_init() {
+  let storage = yield Metrics.Storage("init");
+  let provider = new CrashesProvider();
+  yield provider.init(storage);
+  yield provider.shutdown();
+
+  yield storage.close();
+});
+
+add_task(function test_collect() {
+  let storage = yield Metrics.Storage("collect");
+  let provider = new CrashesProvider();
+  yield provider.init(storage);
+
+  // FUTURE Don't rely on state from previous test.
+  yield provider.collectConstantData();
+
+  let m = provider.getMeasurement("crashes", 1);
+  let values = yield m.getValues();
+  do_check_eq(values.days.size, Object.keys(gPending).length);
+  for each (let date in gPending) {
+    do_check_true(values.days.hasDay(date));
+
+    let value = values.days.getDay(date);
+    do_check_true(value.has("pending"));
+    do_check_true(value.has("submitted"));
+    do_check_eq(value.get("pending"), 1);
+    do_check_eq(value.get("submitted"), 1);
+  }
+
+  let currentState = yield provider.getState("lastCheck");
+  do_check_eq(typeof(currentState), "string");
+  do_check_true(currentState.length > 0);
+  let lastState = currentState;
+
+  // If we collect again, we should get no new data.
+  yield provider.collectConstantData();
+  values = yield m.getValues();
+  for each (let date in gPending) {
+    let day = values.days.getDay(date);
+    do_check_eq(day.get("pending"), 1);
+    do_check_eq(day.get("submitted"), 1);
+  }
+
+  currentState = yield provider.getState("lastCheck");
+  do_check_neq(currentState, lastState);
+  do_check_true(currentState > lastState);
+
+  let now = new Date();
+  let tomorrow = new Date(now.getTime() + MILLISECONDS_PER_DAY);
+  let yesterday = new Date(now.getTime() - MILLISECONDS_PER_DAY);
+
+  let yesterdayID = createFakeCrash(false, yesterday);
+  let tomorrowID = createFakeCrash(false, tomorrow);
+
+  yield provider.collectConstantData();
+  values = yield m.getValues();
+  do_check_eq(values.days.size, 11);
+  do_check_eq(values.days.getDay(tomorrow).get("pending"), 1);
+
+  for each (let date in gPending) {
+    let day = values.days.getDay(date);
+    do_check_eq(day.get("pending"), 1);
+    do_check_eq(day.get("submitted"), 1);
+  }
+
+  yield provider.shutdown();
+  yield storage.close();
+});
+
new file mode 100644
--- /dev/null
+++ b/services/healthreport/tests/xpcshell/test_provider_sessions.js
@@ -0,0 +1,208 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const {utils: Cu} = Components;
+
+
+Cu.import("resource://gre/modules/commonjs/promise/core.js");
+Cu.import("resource://gre/modules/Metrics.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+Cu.import("resource://gre/modules/services-common/utils.js");
+Cu.import("resource://gre/modules/services/healthreport/providers.jsm");
+
+
+function run_test() {
+  run_next_test();
+}
+
+add_test(function test_constructor() {
+  let provider = new SessionsProvider();
+
+  run_next_test();
+});
+
+add_task(function test_init() {
+  let storage = yield Metrics.Storage("init");
+  let provider = new SessionsProvider();
+  yield provider.init(storage);
+  yield provider.shutdown();
+
+  yield storage.close();
+});
+
+function getProvider(name, now=new Date()) {
+  return Task.spawn(function () {
+    let storage = yield Metrics.Storage(name);
+    let provider = new SessionsProvider();
+    monkeypatchStartupInfo(provider, now);
+    yield provider.init(storage);
+
+    throw new Task.Result([provider, storage]);
+  });
+}
+
+function monkeypatchStartupInfo(provider, start=new Date(), offset=500) {
+  Object.defineProperty(provider, "_getStartupInfo", {
+    value: function _getStartupInfo() {
+      return {
+        process: start,
+        main: new Date(start.getTime() + offset),
+        firstPaint: new Date(start.getTime() + 2 * offset),
+        sessionRestored: new Date(start.getTime() + 3 * offset),
+      };
+    }
+  });
+}
+
+add_task(function test_record_current_on_init() {
+  let [provider, storage] = yield getProvider("record_current_on_init");
+
+  let now = new Date();
+
+  let current = provider.getMeasurement("current", 1);
+  let values = yield current.getValues();
+  let fields = values.singular;
+  do_check_eq(fields.size, 6);
+  do_check_eq(fields.get("main")[1], 500);
+  do_check_eq(fields.get("firstPaint")[1], 1000);
+  do_check_eq(fields.get("sessionRestored")[1], 1500);
+  do_check_eq(fields.get("startDay")[1], provider._dateToDays(now));
+  do_check_eq(fields.get("activeTime")[1], 0);
+  do_check_eq(fields.get("totalTime")[1], 0);
+
+  yield provider.shutdown();
+  yield storage.close();
+});
+
+add_task(function test_current_moved_on_shutdown() {
+  let [provider, storage] = yield getProvider("current_moved_on_shutdown");
+  let now = new Date();
+
+  let previous = provider.getMeasurement("previous", 1);
+
+  yield provider.shutdown();
+
+  // This relies on undocumented behavior of the underlying measurement not
+  // being invalidated on provider shutdown. If this breaks, we should rewrite
+  // the test and not hold up implementation changes.
+  let values = yield previous.getValues();
+  do_check_eq(values.days.size, 1);
+  do_check_true(values.days.hasDay(now));
+  let fields = values.days.getDay(now);
+
+  // 3 startup + 2 clean.
+  do_check_eq(fields.size, 5);
+  for (let field of ["cleanActiveTime", "cleanTotalTime", "main", "firstPaint", "sessionRestored"]) {
+    do_check_true(fields.has(field));
+    do_check_true(Array.isArray(fields.get(field)));
+    do_check_eq(fields.get(field).length, 1);
+  }
+
+  do_check_eq(fields.get("main")[0], 500);
+  do_check_eq(fields.get("firstPaint")[0], 1000);
+  do_check_eq(fields.get("sessionRestored")[0], 1500);
+  do_check_true(fields.get("cleanActiveTime")[0] > 0);
+  do_check_true(fields.get("cleanTotalTime")[0] > 0);
+
+  yield storage.close();
+});
+
+add_task(function test_detect_abort() {
+  let [provider, storage] = yield getProvider("detect_abort");
+
+  let now = new Date();
+
+  let m = provider.getMeasurement("current", 1);
+  let original = yield m.getValues().singular;
+
+  let provider2 = new SessionsProvider();
+  monkeypatchStartupInfo(provider2);
+  yield provider2.init(storage);
+
+  let previous = provider.getMeasurement("previous", 1);
+  let values = yield previous.getValues();
+  do_check_true(values.days.hasDay(now));
+  let day = values.days.getDay(now);
+  do_check_eq(day.size, 5);
+  do_check_true(day.has("abortedActiveTime"));
+  do_check_true(day.has("abortedTotalTime"));
+  do_check_eq(day.get("abortedActiveTime")[0], 0);
+  do_check_eq(day.get("abortedTotalTime")[0], 0);
+
+  yield provider.shutdown();
+  yield provider2.shutdown();
+  yield storage.close();
+});
+
+// This isn't a perfect test because we only simulate the observer
+// notifications. We should probably supplement this with a mochitest.
+add_task(function test_record_browser_activity() {
+  let [provider, storage] = yield getProvider("record_browser_activity");
+
+  function waitOnDB () {
+    return provider.enqueueStorageOperation(function () {
+      return storage._connection.execute("SELECT 1");
+    });
+  }
+
+  let current = provider.getMeasurement("current", 1);
+
+  Services.obs.notifyObservers(null, "user-interaction-active", null);
+  yield waitOnDB();
+
+  let values = yield current.getValues();
+  let fields = values.singular;
+  let activeTime = fields.get("activeTime")[1];
+  let totalTime = fields.get("totalTime")[1];
+
+  do_check_eq(activeTime, totalTime);
+  do_check_true(activeTime > 0);
+
+  // Another active should have similar effects.
+  Services.obs.notifyObservers(null, "user-interaction-active", null);
+  yield waitOnDB();
+
+  values = yield current.getValues();
+  fields = values.singular;
+
+  do_check_true(fields.get("activeTime")[1] > activeTime);
+  activeTime = fields.get("activeTime")[1];
+  totalTime = fields.get("totalTime")[1];
+  do_check_eq(activeTime, totalTime);
+
+  // Now send inactive. We should increment total time but not active.
+  Services.obs.notifyObservers(null, "user-interaction-inactive", null);
+  yield waitOnDB();
+  values = yield current.getValues();
+  fields = values.singular;
+  do_check_eq(fields.get("activeTime")[1], activeTime);
+  totalTime = fields.get("totalTime")[1];
+  do_check_true(totalTime > activeTime);
+
+  // If we send an active again, this should be counted as inactive.
+  Services.obs.notifyObservers(null, "user-interaction-active", null);
+  yield waitOnDB();
+  values = yield current.getValues();
+  fields = values.singular;
+
+  do_check_eq(fields.get("activeTime")[1], activeTime);
+  do_check_true(fields.get("totalTime")[1] > totalTime);
+  do_check_neq(fields.get("activeTime")[1], fields.get("totalTime")[1]);
+  activeTime = fields.get("activeTime")[1];
+  totalTime = fields.get("totalTime")[1];
+
+  // Another active should increment active this time.
+  Services.obs.notifyObservers(null, "user-interaction-active", null);
+  yield waitOnDB();
+  values = yield current.getValues();
+  fields = values.singular;
+  do_check_true(fields.get("activeTime")[1] > activeTime);
+  do_check_true(fields.get("totalTime")[1] > totalTime);
+
+  yield provider.shutdown();
+  yield storage.close();
+});
+
--- a/services/healthreport/tests/xpcshell/test_provider_sysinfo.js
+++ b/services/healthreport/tests/xpcshell/test_provider_sysinfo.js
@@ -1,45 +1,40 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 const {interfaces: Ci, results: Cr, utils: Cu} = Components;
 
+Cu.import("resource://gre/modules/Metrics.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/services/healthreport/providers.jsm");
-Cu.import("resource://gre/modules/services/metrics/dataprovider.jsm");
 
 
 function run_test() {
   run_next_test();
 }
 
 add_test(function test_constructor() {
   let provider = new SysInfoProvider();
 
   run_next_test();
 });
 
-add_test(function test_collect_smoketest() {
+add_task(function test_collect_smoketest() {
+  let storage = yield Metrics.Storage("collect_smoketest");
   let provider = new SysInfoProvider();
+  yield provider.init(storage);
 
-  let result = provider.collectConstantMeasurements();
-  do_check_true(result instanceof MetricsCollectionResult);
+  yield provider.collectConstantData();
 
-  result.onFinished(function onFinished() {
-    do_check_eq(result.expectedMeasurements.size, 1);
-    do_check_true(result.expectedMeasurements.has("sysinfo"));
-    do_check_eq(result.measurements.size, 1);
-    do_check_true(result.measurements.has("sysinfo"));
-    do_check_eq(result.errors.length, 0);
+  let m = provider.getMeasurement("sysinfo", 1);
+  let data = yield storage.getMeasurementValues(m.id);
+  let serializer = m.serializer(m.SERIALIZE_JSON);
+  let d = serializer.singular(data.singular);
 
-    let si = result.measurements.get("sysinfo");
-    do_check_true(si.getValue("cpuCount") > 0);
-    do_check_neq(si.getValue("name"), null);
+  do_check_true(d.cpuCount > 0);
+  do_check_neq(d.name, null);
 
-    run_next_test();
-  });
-
-  result.populate(result);
+  yield storage.close();
 });
 
--- a/services/healthreport/tests/xpcshell/xpcshell.ini
+++ b/services/healthreport/tests/xpcshell/xpcshell.ini
@@ -1,11 +1,14 @@
 [DEFAULT]
 head = head.js
 tail =
 
 [test_load_modules.js]
 [test_profile.js]
 [test_policy.js]
 [test_healthreporter.js]
+[test_provider_addons.js]
 [test_provider_appinfo.js]
+[test_provider_crashes.js]
 [test_provider_sysinfo.js]
+[test_provider_sessions.js]
 
--- a/services/makefiles.sh
+++ b/services/makefiles.sh
@@ -5,24 +5,22 @@
 add_makefiles "
   services/Makefile
   services/aitc/Makefile
   services/common/Makefile
   services/crypto/Makefile
   services/crypto/component/Makefile
   services/healthreport/Makefile
   services/metrics/Makefile
-  services/notifications/Makefile
   services/sync/Makefile
   services/sync/locales/Makefile
 "
 
 if [ "$ENABLE_TESTS" ]; then
   add_makefiles "
     services/aitc/tests/Makefile
     services/common/tests/Makefile
     services/crypto/tests/Makefile
     services/healthreport/tests/Makefile
     services/metrics/tests/Makefile
-    services/notifications/tests/Makefile
     services/sync/tests/Makefile
   "
 fi
--- a/services/metrics/Makefile.in
+++ b/services/metrics/Makefile.in
@@ -7,22 +7,27 @@ topsrcdir = @top_srcdir@
 srcdir    = @srcdir@
 VPATH     = @srcdir@
 
 include $(DEPTH)/config/autoconf.mk
 
 modules := \
   collector.jsm \
   dataprovider.jsm \
+  storage.jsm \
   $(NULL)
 
 testing_modules := \
   mocks.jsm \
   $(NULL)
 
+# We install Metrics.jsm into the "main" JSM repository and the rest in
+# services. External consumers should only go through Metrics.jsm.
+EXTRA_JS_MODULES := Metrics.jsm
+
 TEST_DIRS += tests
 
 MODULES_FILES := $(modules)
 MODULES_DEST = $(FINAL_TARGET)/modules/services/metrics
 INSTALL_TARGETS += MODULES
 
 TESTING_JS_MODULES := $(addprefix modules-testing/,$(testing_modules))
 TESTING_JS_MODULE_DIR := services/metrics
new file mode 100644
--- /dev/null
+++ b/services/metrics/Metrics.jsm
@@ -0,0 +1,24 @@
+/* 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 = ["Metrics"];
+
+const {utils: Cu} = Components;
+
+Cu.import("resource://gre/modules/services/metrics/collector.jsm");
+Cu.import("resource://gre/modules/services/metrics/dataprovider.jsm");
+Cu.import("resource://gre/modules/services/metrics/storage.jsm");
+
+
+this.Metrics = {
+  Collector: Collector,
+  Measurement: Measurement,
+  Provider: Provider,
+  Storage: MetricsStorageBackend,
+  dateToDays: dateToDays,
+  daysToDate: daysToDate,
+};
+
--- a/services/metrics/collector.jsm
+++ b/services/metrics/collector.jsm
@@ -1,166 +1,211 @@
 /* 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 = ["MetricsCollector"];
+this.EXPORTED_SYMBOLS = ["Collector"];
 
 const {utils: Cu} = Components;
 
 Cu.import("resource://gre/modules/commonjs/promise/core.js");
+Cu.import("resource://gre/modules/Task.jsm");
 Cu.import("resource://services-common/log4moz.js");
 Cu.import("resource://services-common/utils.js");
 Cu.import("resource://gre/modules/services/metrics/dataprovider.jsm");
 
 
 /**
  * Handles and coordinates the collection of metrics data from providers.
  *
- * This provides an interface for managing `MetricsProvider` instances. It
+ * This provides an interface for managing `Metrics.Provider` instances. It
  * provides APIs for bulk collection of data.
  */
-this.MetricsCollector = function MetricsCollector() {
-  this._log = Log4Moz.repository.getLogger("Metrics.MetricsCollector");
+this.Collector = function (storage) {
+  this._log = Log4Moz.repository.getLogger("Services.Metrics.Collector");
 
-  this._providers = [];
-  this.collectionResults = new Map();
+  this._providers = new Map();
+  this._storage = storage;
+
+  this._providerInitQueue = [];
+  this._providerInitializing = false;
   this.providerErrors = new Map();
 }
 
-MetricsCollector.prototype = {
+Collector.prototype = Object.freeze({
+  get providers() {
+    let providers = [];
+    for (let [name, entry] of this._providers) {
+      providers.push(entry.provider);
+    }
+
+    return providers;
+  },
+
   /**
    * Registers a `MetricsProvider` with this collector.
    *
    * Once a `MetricsProvider` is registered, data will be collected from it
    * whenever we collect data.
    *
+   * The returned value is a promise that will be resolved once registration
+   * is complete.
+   *
+   * Providers are initialized as part of registration by calling
+   * provider.init().
+   *
    * @param provider
-   *        (MetricsProvider) The provider instance to register.
+   *        (Metrics.Provider) The provider instance to register.
+   *
+   * @return Promise<null>
    */
-  registerProvider: function registerProvider(provider) {
-    if (!(provider instanceof MetricsProvider)) {
-      throw new Error("argument must be a MetricsProvider instance.");
+  registerProvider: function (provider) {
+    if (!(provider instanceof Provider)) {
+      throw new Error("Argument must be a Provider instance.");
+    }
+
+    if (this._providers.has(provider.name)) {
+      return Promise.resolve();
+    }
+
+    let deferred = Promise.defer();
+    this._providerInitQueue.push([provider, deferred]);
+
+    if (this._providerInitQueue.length == 1) {
+      this._popAndInitProvider();
+    }
+
+    return deferred.promise;
+  },
+
+  _popAndInitProvider: function () {
+    if (!this._providerInitQueue.length || this._providerInitializing) {
+      return;
     }
 
-    for (let p of this._providers) {
-      if (p.provider == provider) {
-        return;
-      }
+    let [provider, deferred] = this._providerInitQueue.pop();
+    this._providerInitializing = true;
+
+    this._log.info("Initializing provider with storage: " + provider.name);
+    let initPromise;
+    try {
+      initPromise = provider.init(this._storage);
+    } catch (ex) {
+      this._log.warn("Provider failed to initialize: " +
+                     CommonUtils.exceptionStr(ex));
+      this._providerInitializing = false;
+      deferred.reject(ex);
+      this._popAndInitProvider();
+      return;
     }
 
-    this._providers.push({
-      provider: provider,
-      constantsCollected: false,
-    });
+    initPromise.then(
+      function onSuccess(result) {
+        this._log.info("Provider finished initialization: " + provider.name);
+        this._providerInitializing = false;
+
+        this._providers.set(provider.name, {
+          provider: provider,
+          constantsCollected: false,
+        });
+
+        this.providerErrors.set(provider.name, []);
 
-    this.providerErrors.set(provider.name, []);
+        deferred.resolve(result);
+        this._popAndInitProvider();
+      }.bind(this),
+      function onError(error) {
+        this._log.warn("Provider initialization failed: " +
+                       CommonUtils.exceptionStr(error));
+        this._providerInitializing = false;
+        deferred.reject(error);
+        this._popAndInitProvider();
+      }.bind(this)
+    );
+
   },
 
   /**
    * Collects all constant measurements from all providers.
    *
    * Returns a Promise that will be fulfilled once all data providers have
    * provided their constant data. A side-effect of this promise fulfillment
    * is that the collector is populated with the obtained collection results.
-   * The resolved value to the promise is this `MetricsCollector` instance.
+   * The resolved value to the promise is this `Collector` instance.
    */
-  collectConstantMeasurements: function collectConstantMeasurements() {
+  collectConstantData: function () {
     let promises = [];
 
-    for (let provider of this._providers) {
-      let name = provider.provider.name;
-
-      if (provider.constantsCollected) {
+    for (let [name, entry] of this._providers) {
+      if (entry.constantsCollected) {
         this._log.trace("Provider has already provided constant data: " +
                         name);
         continue;
       }
 
-      let result;
+      let collectPromise;
       try {
-        result = provider.provider.collectConstantMeasurements();
+        collectPromise = entry.provider.collectConstantData();
       } catch (ex) {
         this._log.warn("Exception when calling " + name +
-                       ".collectConstantMeasurements: " +
+                       ".collectConstantData: " +
                        CommonUtils.exceptionStr(ex));
         this.providerErrors.get(name).push(ex);
         continue;
       }
 
-      if (!result) {
-        this._log.trace("Provider does not provide constant data: " + name);
-        continue;
+      if (!collectPromise) {
+        throw new Error("Provider does not return a promise from " +
+                        "collectConstantData():" + name);
       }
 
-      try {
-        this._log.debug("Populating constant measurements: " + name);
-        result.populate(result);
-      } catch (ex) {
-        this._log.warn("Exception when calling " + name + ".populate(): " +
-                       CommonUtils.exceptionStr(ex));
-        result.addError(ex);
-        promises.push(Promise.resolve(result));
-        continue;
-      }
-
-      // Chain an invisible promise that updates state.
-      let promise = result.onFinished(function onFinished(result) {
-        provider.constantsCollected = true;
+      let promise = collectPromise.then(function onCollected(result) {
+        entry.constantsCollected = true;
 
         return Promise.resolve(result);
       });
 
-      promises.push(promise);
+      promises.push([name, promise]);
     }
 
     return this._handleCollectionPromises(promises);
   },
 
   /**
    * Handles promises returned by the collect* functions.
    *
    * This consumes the data resolved by the promises and returns a new promise
    * that will be resolved once all promises have been resolved.
+   *
+   * The promise is resolved even if one of the underlying collection
+   * promises is rejected.
    */
-  _handleCollectionPromises: function _handleCollectionPromises(promises) {
+  _handleCollectionPromises: function (promises) {
     if (!promises.length) {
       return Promise.resolve(this);
     }
 
     let deferred = Promise.defer();
     let finishedCount = 0;
 
-    let onResult = function onResult(result) {
-      try {
-        this._log.debug("Got result for " + result.name);
-
-        if (this.collectionResults.has(result.name)) {
-          this.collectionResults.get(result.name).aggregate(result);
-        } else {
-          this.collectionResults.set(result.name, result);
-        }
-      } finally {
-        finishedCount++;
-        if (finishedCount >= promises.length) {
-          deferred.resolve(this);
-        }
+    let onComplete = function () {
+      finishedCount++;
+      if (finishedCount >= promises.length) {
+        deferred.resolve(this);
       }
     }.bind(this);
 
-    let onError = function onError(error) {
-      this._log.warn("Error when handling result: " +
-                     CommonUtils.exceptionStr(error));
-      deferred.reject(error);
-    }.bind(this);
-
-    for (let promise of promises) {
-      promise.then(onResult, onError);
+    for (let [name, promise] of promises) {
+      let onError = function (error) {
+        this._log.warn("Collection promise was rejected: " +
+                       CommonUtils.exceptionStr(error));
+        this.providerErrors.get(name).push(error);
+        onComplete();
+      }.bind(this);
+      promise.then(onComplete, onError);
     }
 
     return deferred.promise;
   },
-};
+});
 
-Object.freeze(MetricsCollector.prototype);
-
--- a/services/metrics/dataprovider.jsm
+++ b/services/metrics/dataprovider.jsm
@@ -1,493 +1,535 @@
 /* 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 = [
-  "MetricsCollectionResult",
-  "MetricsMeasurement",
-  "MetricsProvider",
+  "Measurement",
+  "Provider",
 ];
 
 const {utils: Cu} = Components;
 
 Cu.import("resource://gre/modules/commonjs/promise/core.js");
+Cu.import("resource://gre/modules/Task.jsm");
 Cu.import("resource://services-common/log4moz.js");
+Cu.import("resource://services-common/utils.js");
+
+
+const MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000;
 
 
 /**
- * Represents a measurement of data.
- *
- * This is how data is recorded and represented. Each instance of this type
- * represents a related set of data.
- *
- * Each data set has some basic metadata associated with it. This includes a
- * name and version.
+ * Represents a collection of related pieces/fields of data.
  *
- * This type is meant to be an abstract base type. Child types should define
- * a `fields` property which is a mapping of field names to metadata describing
- * that field. This field constitutes the "schema" of the measurement/type.
+ * This is an abstract base type. Providers implement child types that
+ * implement core functions such as `registerStorage`.
  *
- * Data is added to instances by calling `setValue()`. Values are validated
- * against the schema at add time.
- *
- * Field Specification
- * ===================
+ * This type provides the primary interface for storing, retrieving, and
+ * serializing data.
  *
- * The `fields` property is a mapping of string field names to a mapping of
- * metadata describing the field. This mapping can have the following
- * properties:
+ * Each derived type must define a `name` and `version` property. These must be
+ * a string name and integer version, respectively. The `name` is used to
+ * identify the measurement within a `Provider`. The version is to denote the
+ * behavior of the `Measurement` and the composition of its fields over time.
+ * When a new field is added or the behavior of an existing field changes
+ * (perhaps the method for storing it has changed), the version should be
+ * incremented.
  *
- *   type -- A string corresponding to the TYPE_* property name describing a
- *           field type. The TYPE_* properties are defined on this type. e.g.
- *           "TYPE_STRING".
- *
- *   optional -- If true, this field is optional. If omitted, the field is
- *               required.
+ * Each measurement consists of a set of named fields. Each field is primarily
+ * identified by a string name, which must be unique within the measurement.
  *
- * @param name
- *        (string) Name of this data set.
- * @param version
- *        (Number) Integer version of the data in this set.
+ * For fields backed by the SQLite metrics storage backend, fields must have a
+ * strongly defined type. Valid types include daily counters, daily discrete
+ * text values, etc. See `MetricsStorageSqliteBackend.FIELD_*`.
+ *
+ * FUTURE: provide hook points for measurements to supplement with custom
+ * storage needs.
  */
-this.MetricsMeasurement = function MetricsMeasurement(name, version) {
-  if (!this.fields) {
-    throw new Error("fields not defined on instance. You are likely using " +
-                    "this type incorrectly.");
-  }
-
-  if (!name) {
-    throw new Error("Must define a name for this measurement.");
+this.Measurement = function () {
+  if (!this.name) {
+    throw new Error("Measurement must have a name.");
   }
 
-  if (!version) {
-    throw new Error("Must define a version for this measurement.");
+  if (!this.version) {
+    throw new Error("Measurement must have a version.");
+  }
+
+  if (!Number.isInteger(this.version)) {
+    throw new Error("Measurement's version must be an integer: " + this.version);
   }
 
-  if (!Number.isInteger(version)) {
-    throw new Error("version must be an integer: " + version);
-  }
+  this._log = Log4Moz.repository.getLogger("Services.Metrics.Measurement." + this.name);
+
+  this.id = null;
+  this.storage = null;
+  this._fieldsByName = new Map();
 
-  this.name = name;
-  this.version = version;
-
-  this.values = new Map();
+  this._serializers = {};
+  this._serializers[this.SERIALIZE_JSON] = {
+    singular: this._serializeJSONSingular.bind(this),
+    daily: this._serializeJSONDay.bind(this),
+  };
 }
 
-MetricsMeasurement.prototype = {
-  /**
-   * An unsigned integer field stored in 32 bits.
-   *
-   * This holds values from 0 to 2^32 - 1.
-   */
-  TYPE_UINT32: {
-    validate: function validate(value) {
-      if (!Number.isInteger(value)) {
-        throw new Error("UINT32 field expects an integer. Got " + value);
-      }
-
-      if (value < 0) {
-        throw new Error("UINT32 field expects a positive integer. Got " + value);
-      }
-
-      if (value >= 0xffffffff) {
-        throw new Error("Value is too large to fit within 32 bits: " + value);
-      }
-    },
-  },
+Measurement.prototype = Object.freeze({
+  SERIALIZE_JSON: "json",
 
   /**
-   * A string field.
+   * Configures the storage backend so that it can store this measurement.
+   *
+   * Implementations must return a promise which is resolved when storage has
+   * been configured.
    *
-   * Values must be valid UTF-8 strings.
+   * Most implementations will typically call into this.registerStorageField()
+   * to configure fields in storage.
+   *
+   * FUTURE: Provide method for upgrading from older measurement versions.
    */
-  TYPE_STRING: {
-    validate: function validate(value) {
-      if (typeof(value) != "string") {
-        throw new Error("STRING field expects a string. Got " + typeof(value));
-      }
-    },
+  configureStorage: function () {
+    throw new Error("configureStorage() must be implemented.");
   },
 
   /**
-   * Set the value of a field.
+   * Obtain a serializer for this measurement.
+   *
+   * Implementations should return an object with the following keys:
+   *
+   *   singular -- Serializer for singular data.
+   *   daily -- Serializer for daily data.
    *
-   * This is ultimately how fields are set. All field sets should go through
-   * this function.
+   * Each item is a function that takes a single argument: the data to
+   * serialize. The passed data is a subset of that returned from
+   * this.getValues(). For "singular," data.singular is passed. For "daily",
+   * data.days.get(<day>) is passed.
    *
-   * Values are validated when they are set. If the value passed does not
-   * validate against the field's specification, an Error will be thrown.
+   * This function receives a single argument: the serialization format we
+   * are requesting. This is one of the SERIALIZE_* constants on this base type.
    *
-   * @param name
-   *        (string) The name of the field whose value to set.
-   * @param value
-   *        The value to set the field to.
+   * For SERIALIZE_JSON, the function should return an object that
+   * JSON.stringify() knows how to handle. This could be an anonymous object or
+   * array or any object with a property named `toJSON` whose value is a
+   * function. The returned object will be added to a larger document
+   * containing the results of all `serialize` calls.
+   *
+   * The default implementation knows how to serialize built-in types using
+   * very simple logic. If small encoding size is a goal, the default
+   * implementation may not be suitable. If an unknown field type is
+   * encountered, the default implementation will error.
+   *
+   * @param format
+   *        (string) A SERIALIZE_* constant defining what serialization format
+   *        to use.
    */
-  setValue: function setValue(name, value) {
-    if (!this.fields[name]) {
-      throw new Error("Attempting to set unknown field: " + name);
+  serializer: function (format) {
+    if (!(format in this._serializers)) {
+      throw new Error("Don't know how to serialize format: " + format);
     }
 
-    let type = this.fields[name].type;
+    return this._serializers[format];
+  },
 
-    if (!(type in this)) {
-      throw new Error("Unknown field type: " + type);
+  hasField: function (name) {
+    return this._fieldsByName.has(name);
+  },
+
+  fieldID: function (name) {
+    let entry = this._fieldsByName.get(name);
+
+    if (!entry) {
+      throw new Error("Unknown field: " + name);
     }
 
-    this[type].validate(value);
-    this.values.set(name, value);
+    return entry[0];
   },
 
-  /**
-   * Obtain the value of a named field.
-   *
-   * @param name
-   *        (string) The name of the field to retrieve.
-   */
-  getValue: function getValue(name) {
-    return this.values.get(name);
+  fieldType: function (name) {
+    let entry = this._fieldsByName.get(name);
+
+    if (!entry) {
+      throw new Error("Unknown field: " + name);
+    }
+
+    return entry[1];
   },
 
   /**
-   * Validate that this instance is in conformance with the specification.
+   * Register a named field with storage that's attached to this measurement.
    *
-   * This ensures all required fields are present. Field value validation
-   * occurs when individual fields are set.
+   * This is typically called during `configureStorage`. The `Measurement`
+   * implementation passes the field name and its type (one of the
+   * storage.FIELD_* constants). The storage backend then allocates space
+   * for this named field. A side-effect of calling this is that the field's
+   * storage ID is stored in this._fieldsByName and subsequent calls to the
+   * storage modifiers below will know how to reference this field in the
+   * storage backend.
+   *
+   * @param name
+   *        (string) The name of the field being registered.
+   * @param type
+   *        (string) A field type name. This is typically one of the
+   *        storage.FIELD_* constants. It could also be a custom type
+   *        (presumably registered by this measurement or provider).
    */
-  validate: function validate() {
-    for (let field in this.fields) {
-      let spec = this.fields[field];
+  registerStorageField: function (name, type) {
+    this._log.debug("Registering field: " + name + " " + type);
+
+    let deferred = Promise.defer();
 
-      if (!spec.optional && !(field in this.values)) {
-        throw new Error("Required field not defined: " + field);
-      }
-    }
+    let self = this;
+    this.storage.registerField(this.id, name, type).then(
+      function onSuccess(id) {
+        self._fieldsByName.set(name, [id, type]);
+        deferred.resolve();
+      }, deferred.reject);
+
+    return deferred.promise;
   },
 
-  toJSON: function toJSON() {
-    let fields = {};
-    for (let [k, v] of this.values) {
-      fields[k] = v;
-    }
+  incrementDailyCounter: function (field, date=new Date()) {
+    return this.storage.incrementDailyCounterFromFieldID(this.fieldID(field),
+                                                         date);
+  },
 
-    return {
-      name: this.name,
-      version: this.version,
-      fields: fields,
-    };
+  addDailyDiscreteNumeric: function (field, value, date=new Date()) {
+    return this.storage.addDailyDiscreteNumericFromFieldID(
+                          this.fieldID(field), value, date);
   },
-};
 
-Object.freeze(MetricsMeasurement.prototype);
-
+  addDailyDiscreteText: function (field, value, date=new Date()) {
+    return this.storage.addDailyDiscreteTextFromFieldID(
+                          this.fieldID(field), value, date);
+  },
 
-/**
- * Entity which provides metrics data for recording.
- *
- * This essentially provides an interface that different systems must implement
- * to provide collected metrics data.
- *
- * This type consists of various collect* functions. These functions are called
- * by the metrics collector at different points during the application's
- * lifetime. These functions return a `MetricsCollectionResult` instance.
- * This type behaves a lot like a promise. It has a `onFinished()` that can chain
- * deferred events until after the result is populated.
- *
- * Implementations of collect* functions should call `createResult()` to create
- * a new `MetricsCollectionResult` instance. They should then register
- * expected measurements with this instance, define a `populate` function on
- * it, then return the instance.
- *
- * It is important for the collect* functions to just create the empty
- * `MetricsCollectionResult` and nothing more. This is to enable the callee
- * to handle errors gracefully. If the collect* function were to raise, the
- * callee may not receive a `MetricsCollectionResult` instance and it would not
- * know what data is missing.
- *
- * See the documentation for `MetricsCollectionResult` for details on how
- * to perform population.
- *
- * Receivers of created `MetricsCollectionResult` instances should wait
- * until population has finished. They can do this by chaining on to the
- * promise inside that instance by calling `onFinished()`.
- *
- * The collect* functions can return null to signify that they will never
- * provide any data. This is the default implementation. An implemented
- * collect* function should *never* return null. Instead, it should return
- * a `MetricsCollectionResult` with expected measurements that has finished
- * populating (i.e. an empty result).
- *
- * @param name
- *        (string) The name of this provider.
- */
-this.MetricsProvider = function MetricsProvider(name) {
-  if (!name) {
-    throw new Error("MetricsProvider must have a name.");
-  }
+  setLastNumeric: function (field, value, date=new Date()) {
+    return this.storage.setLastNumericFromFieldID(this.fieldID(field), value,
+                                                  date);
+  },
 
-  if (typeof(name) != "string") {
-    throw new Error("name must be a string. Got: " + typeof(name));
-  }
-
-  this._log = Log4Moz.repository.getLogger("Services.Metrics.MetricsProvider");
-
-  this.name = name;
-}
+  setLastText: function (field, value, date=new Date()) {
+    return this.storage.setLastTextFromFieldID(this.fieldID(field), value,
+                                               date);
+  },
 
-MetricsProvider.prototype = {
-  /**
-   * Collects constant measurements.
-   *
-   * Constant measurements are data that doesn't change during the lifetime of
-   * the application/process. The metrics collector only needs to call this
-   * once per `MetricsProvider` instance per process lifetime.
-   */
-  collectConstantMeasurements: function collectConstantMeasurements() {
-    return null;
+  setDailyLastNumeric: function (field, value, date=new Date()) {
+    return this.storage.setDailyLastNumericFromFieldID(this.fieldID(field),
+                                                       value, date);
+  },
+
+  setDailyLastText: function (field, value, date=new Date()) {
+    return this.storage.setDailyLastTextFromFieldID(this.fieldID(field),
+                                                    value, date);
   },
 
   /**
-   * Create a new `MetricsCollectionResult` tied to this provider.
+   * Obtain all values stored for this measurement.
+   *
+   * The default implementation obtains all known types from storage. If the
+   * measurement provides custom types or stores values somewhere other than
+   * storage, it should define its own implementation.
+   *
+   * This returns a promise that resolves to a data structure which is
+   * understood by the measurement's serialize() function.
    */
-  createResult: function createResult() {
-    return new MetricsCollectionResult(this.name);
+  getValues: function () {
+    return this.storage.getMeasurementValues(this.id);
+  },
+
+  deleteLastNumeric: function (field) {
+    return this.storage.deleteLastNumericFromFieldID(this.fieldID(field));
+  },
+
+  deleteLastText: function (field) {
+    return this.storage.deleteLastTextFromFieldID(this.fieldID(field));
   },
-};
+
+  _serializeJSONSingular: function (data) {
+    let result = {};
+
+    for (let [field, data] of data) {
+      // There could be legacy fields in storage we no longer care about.
+      if (!this._fieldsByName.has(field)) {
+        continue;
+      }
+
+      let type = this.fieldType(field);
+
+      switch (type) {
+        case this.storage.FIELD_LAST_NUMERIC:
+        case this.storage.FIELD_LAST_TEXT:
+          result[field] = data[1];
+          break;
+
+        case this.storage.FIELD_DAILY_COUNTER:
+        case this.storage.FIELD_DAILY_DISCRETE_NUMERIC:
+        case this.storage.FIELD_DAILY_DISCRETE_TEXT:
+        case this.storage.FIELD_DAILY_LAST_NUMERIC:
+        case this.storage.FIELD_DAILY_LAST_TEXT:
+          continue;
+
+        default:
+          throw new Error("Unknown field type: " + type);
+      }
+    }
 
-Object.freeze(MetricsProvider.prototype);
+    return result;
+  },
+
+  _serializeJSONDay: function (data) {
+    let result = {};
+
+    for (let [field, data] of data) {
+      if (!this._fieldsByName.has(field)) {
+        continue;
+      }
+
+      let type = this.fieldType(field);
+
+      switch (type) {
+        case this.storage.FIELD_DAILY_COUNTER:
+        case this.storage.FIELD_DAILY_DISCRETE_NUMERIC:
+        case this.storage.FIELD_DAILY_DISCRETE_TEXT:
+        case this.storage.FIELD_DAILY_LAST_NUMERIC:
+        case this.storage.FIELD_DAILY_LAST_TEXT:
+          result[field] = data;
+          break;
+
+        case this.storage.FIELD_LAST_NUMERIC:
+        case this.storage.FIELD_LAST_TEXT:
+          continue;
+
+        default:
+          throw new Error("Unknown field type: " + type);
+      }
+    }
+
+    return result;
+  },
+});
 
 
 /**
- * Holds the result of metrics collection.
+ * An entity that emits data.
+ *
+ * A `Provider` consists of a string name (must be globally unique among all
+ * known providers) and a set of `Measurement` instances.
  *
- * This is the type eventually returned by the MetricsProvider.collect*
- * functions. It holds all results and any state/errors that occurred while
- * collecting.
+ * The main role of a `Provider` is to produce metrics data and to store said
+ * data in the storage backend.
+ *
+ * Metrics data collection is initiated either by a collector calling a
+ * `collect*` function on `Provider` instances or by the `Provider` registering
+ * to some external event and then reacting whenever they occur.
  *
- * This type is essentially a container for `MetricsMeasurement` instances that
- * provides some smarts useful for capturing state.
+ * `Provider` implementations interface directly with a storage backend. For
+ * common stored values (daily counters, daily discrete values, etc),
+ * implementations should interface with storage via the various helper
+ * functions on the `Measurement` instances. For custom stored value types,
+ * implementations will interact directly with the low-level storage APIs.
  *
- * The first things consumers of new instances should do is define the set of
- * expected measurements this result will contain via `expectMeasurement`. If
- * population of this instance is aborted or times out, downstream consumers
- * will know there is missing data.
+ * Because multiple providers exist and could be responding to separate
+ * external events simultaneously and because not all operations performed by
+ * storage can safely be performed in parallel, writing directly to storage at
+ * event time is dangerous. Therefore, interactions with storage must be
+ * deferred until it is safe to perform them.
+ *
+ * This typically looks something like:
  *
- * Next, they should define the `populate` property to a function that
- * populates the instance.
+ *   // This gets called when an external event worthy of recording metrics
+ *   // occurs. The function receives a numeric value associated with the event.
+ *   function onExternalEvent (value) {
+ *     let now = new Date();
+ *     let m = this.getMeasurement("foo", 1);
+ *
+ *     this.enqueueStorageOperation(function storeExternalEvent() {
  *
- * The `populate` function implementation should add empty `MetricsMeasurement`
- * instances to the result via `addMeasurement`. Then, it should populate these
- * measurements via `setValue`.
+ *       // We interface with storage via the `Measurement` helper functions.
+ *       // These each return a promise that will be resolved when the
+ *       // operation finishes. We rely on behavior of storage where operations
+ *       // are executed single threaded and sequentially. Therefore, we only
+ *       // need to return the final promise.
+ *       m.incrementDailyCounter("foo", now);
+ *       return m.addDailyDiscreteNumericValue("my_value", value, now);
+ *     }.bind(this));
  *
- * It is preferred to populate via this type instead of directly on
- * `MetricsMeasurement` instances so errors with data population can be
- * captured and reported.
+ *   }
+ *
+ *
+ * `Provider` is an abstract base class. Implementations must define a few
+ * properties:
  *
- * Once population has finished, `finish()` must be called.
+ *   name
+ *     The `name` property should be a string defining the provider's name. The
+ *     name must be globally unique for the application. The name is used as an
+ *     identifier to distinguish providers from each other.
  *
- * @param name
- *        (string) The name of the provider this result came from.
+ *   measurementTypes
+ *     This must be an array of `Measurement`-derived types. Note that elements
+ *     in the array are the type functions, not instances. Instances of the
+ *     `Measurement` are created at run-time by the `Provider` and are bound
+ *     to the provider and to a specific storage backend.
  */
-this.MetricsCollectionResult = function MetricsCollectionResult(name) {
-  if (!name || typeof(name) != "string") {
-    throw new Error("Must provide name argument to MetricsCollectionResult.");
+this.Provider = function () {
+  if (!this.name) {
+    throw new Error("Provider must define a name.");
+  }
+
+  if (!Array.isArray(this.measurementTypes)) {
+    throw new Error("Provider must define measurement types.");
   }
 
-  this._log = Log4Moz.repository.getLogger("Services.Metrics.MetricsCollectionResult");
-
-  this.name = name;
+  this._log = Log4Moz.repository.getLogger("Services.Metrics.Provider." + this.name);
 
-  this.measurements = new Map();
-  this.expectedMeasurements = new Set();
-  this.errors = [];
-
-  this.populate = function populate() {
-    throw new Error("populate() must be defined on MetricsCollectionResult " +
-                    "instance.");
-  };
-
-  this._deferred = Promise.defer();
+  this.measurements = null;
+  this.storage = null;
 }
 
-MetricsCollectionResult.prototype = {
+Provider.prototype = Object.freeze({
   /**
-   * The Set of `MetricsMeasurement` names currently missing from this result.
+   * Obtain a `Measurement` from its name and version.
+   *
+   * If the measurement is not found, an Error is thrown.
    */
-  get missingMeasurements() {
-    let missing = new Set();
+  getMeasurement: function (name, version) {
+    if (!Number.isInteger(version)) {
+      throw new Error("getMeasurement expects an integer version. Got: " + version);
+    }
+
+    let m = this.measurements.get([name, version].join(":"));
 
-    for (let name of this.expectedMeasurements) {
-      if (this.measurements.has(name)) {
-        continue;
-      }
+    if (!m) {
+      throw new Error("Unknown measurement: " + name + " v" + version);
+    }
+
+    return m;
+  },
 
-      missing.add(name);
+  init: function (storage) {
+    if (this.storage !== null) {
+      throw new Error("Provider() not called. Did the sub-type forget to call it?");
+    }
+
+    if (this.storage) {
+      throw new Error("Provider has already been initialized.");
     }
 
-    return missing;
-  },
+    this.measurements = new Map();
+    this.storage = storage;
+
+    let self = this;
+    return Task.spawn(function init() {
+      for (let measurementType of self.measurementTypes) {
+        let measurement = new measurementType();
+
+        measurement.provider = self;
+        measurement.storage = self.storage;
+
+        let id = yield storage.registerMeasurement(self.name, measurement.name,
+                                                   measurement.version);
 
-  /**
-   * Record that this result is expected to provide a named measurement.
-   *
-   * This function should be called ASAP on new `MetricsCollectionResult`
-   * instances. It defines expectations about what data should be present.
-   *
-   * @param name
-   *        (string) The name of the measurement this result should contain.
-   */
-  expectMeasurement: function expectMeasurement(name) {
-    this.expectedMeasurements.add(name);
+        measurement.id = id;
+
+        yield measurement.configureStorage();
+
+        self.measurements.set([measurement.name, measurement.version].join(":"),
+                              measurement);
+      }
+
+      let promise = self.onInit();
+
+      if (!promise || typeof(promise.then) != "function") {
+        throw new Error("onInit() does not return a promise.");
+      }
+
+      yield promise;
+    });
   },
 
-  /**
-   * Add a `MetricsMeasurement` to this result.
-   */
-  addMeasurement: function addMeasurement(data) {
-    if (!(data instanceof MetricsMeasurement)) {
-      throw new Error("addMeasurement expects a MetricsMeasurement instance.");
+  shutdown: function () {
+    let promise = this.onShutdown();
+
+    if (!promise || typeof(promise.then) != "function") {
+      throw new Error("onShutdown implementation does not return a promise.");
     }
 
-    if (!this.expectedMeasurements.has(data.name)) {
-      throw new Error("Not expecting this measurement: " + data.name);
-    }
-
-    if (this.measurements.has(data.name)) {
-      throw new Error("Measurement of this name already present: " + data.name);
-    }
-
-    this.measurements.set(data.name, data);
+    return promise;
   },
 
   /**
-   * Sets the value of a field in a registered measurement instance.
-   *
-   * This is a convenience function to set a field on a measurement. If an
-   * error occurs, it will record that error in the errors container.
-   *
-   * Attempting to set a value on a measurement that does not exist results
-   * in an Error being thrown. Attempting a bad assignment on an existing
-   * measurement will not throw unless `rethrow` is true.
+   * Hook point for implementations to perform initialization activity.
    *
-   * @param name
-   *        (string) The `MetricsMeasurement` on which to set the value.
-   * @param field
-   *        (string) The field we are setting.
-   * @param value
-   *        The value being set.
-   * @param rethrow
-   *        (bool) Whether to rethrow any errors encountered.
+   * If a `Provider` instance needs to register observers, etc, it should
+   * implement this function.
    *
-   * @return bool
-   *         Whether the assignment was successful.
+   * Implementations should return a promise which is resolved when
+   * initialization activities have completed.
    */
-  setValue: function setValue(name, field, value, rethrow=false) {
-    let m = this.measurements.get(name);
-    if (!m) {
-      throw new Error("Attempting to operate on an undefined measurement: " +
-                      name);
-    }
-
-    try {
-      m.setValue(field, value);
-      return true;
-    } catch (ex) {
-      this.addError(ex);
-
-      if (rethrow) {
-        throw ex;
-      }
-
-      return false;
-    }
+  onInit: function () {
+    return Promise.resolve();
   },
 
   /**
-   * Record an error that was encountered when populating this result.
+   * Hook point for shutdown of instances.
+   *
+   * This is the opposite of `onInit`. If a `Provider` needs to unregister
+   * observers, etc, this is where it should do it.
+   *
+   * Implementations should return a promise which is resolved when
+   * shutdown activities have completed.
    */
-  addError: function addError(error) {
-    this.errors.push(error);
+  onShutdown: function () {
+    return Promise.resolve();
+  },
+
+  /**
+   * Collects data that doesn't change during the application's lifetime.
+   *
+   * Implementations should return a promise that resolves when all data has
+   * been collected and storage operations have been finished.
+   */
+  collectConstantData: function () {
+    return Promise.resolve();
   },
 
   /**
-   * Aggregate another MetricsCollectionResult into this one.
+   * Queue a deferred storage operation.
    *
-   * Instances can only be aggregated together if they belong to the same
-   * provider (they have the same name).
+   * Deferred storage operations are the preferred method for providers to
+   * interact with storage. When collected data is to be added to storage,
+   * the provider creates a function that performs the necessary storage
+   * interactions and then passes that function to this function. Pending
+   * storage operations will be executed sequentially by a coordinator.
+   *
+   * The passed function should return a promise which will be resolved upon
+   * completion of storage interaction.
    */
-  aggregate: function aggregate(other) {
-    if (!(other instanceof MetricsCollectionResult)) {
-      throw new Error("aggregate expects a MetricsCollectionResult instance.");
-    }
-
-    if (this.name != other.name) {
-      throw new Error("Can only aggregate MetricsCollectionResult from " +
-                      "the same provider. " + this.name + " != " + other.name);
-    }
-
-    for (let name of other.expectedMeasurements) {
-      this.expectedMeasurements.add(name);
-    }
-
-    for (let [name, m] of other.measurements) {
-      if (this.measurements.has(name)) {
-        throw new Error("Incoming result has same measurement as us: " + name);
-      }
-
-      this.measurements.set(name, m);
-    }
-
-    this.errors = this.errors.concat(other.errors);
+  enqueueStorageOperation: function (func) {
+    return this.storage.enqueueOperation(func);
   },
 
-  toJSON: function toJSON() {
-    let o = {
-      measurements: {},
-      missing: [],
-      errors: [],
-    };
-
-    for (let [name, value] of this.measurements) {
-      o.measurements[name] = value;
-    }
-
-    for (let missing of this.missingMeasurements) {
-      o.missing.push(missing);
-    }
-
-    for (let error of this.errors) {
-      if (error.message) {
-        o.errors.push(error.message);
-      } else {
-        o.errors.push(error);
-      }
-    }
-
-    return o;
+  getState: function (key) {
+    let name = this.name;
+    let storage = this.storage;
+    return storage.enqueueOperation(function get() {
+      return storage.getProviderState(name, key);
+    });
   },
 
-  /**
-   * Signal that population of the result has finished.
-   *
-   * This will resolve the internal promise.
-   */
-  finish: function finish() {
-    this._deferred.resolve(this);
+  setState: function (key, value) {
+    let name = this.name;
+    let storage = this.storage;
+    return storage.enqueueOperation(function set() {
+      return storage.setProviderState(name, key, value);
+    });
   },
 
-  /**
-   * Chain deferred behavior until after the result has finished population.
-   *
-   * This is a wrapped around the internal promise's `then`.
-   *
-   * We can't call this "then" because the core promise library will get
-   * confused.
-   */
-  onFinished: function onFinished(onFulfill, onError) {
-    return this._deferred.promise.then(onFulfill, onError);
+  _dateToDays: function (date) {
+    return Math.floor(date.getTime() / MILLISECONDS_PER_DAY);
   },
-};
 
-Object.freeze(MetricsCollectionResult.prototype);
+  _daysToDate: function (days) {
+    return new Date(days * MILLISECONDS_PER_DAY);
+  },
+});
 
--- a/services/metrics/modules-testing/mocks.jsm
+++ b/services/metrics/modules-testing/mocks.jsm
@@ -6,69 +6,84 @@
 
 this.EXPORTED_SYMBOLS = [
   "DummyMeasurement",
   "DummyProvider",
 ];
 
 const {utils: Cu} = Components;
 
-Cu.import("resource://gre/modules/services/metrics/dataprovider.jsm");
+Cu.import("resource://gre/modules/commonjs/promise/core.js");
+Cu.import("resource://gre/modules/Metrics.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
 
 this.DummyMeasurement = function DummyMeasurement(name="DummyMeasurement") {
-  MetricsMeasurement.call(this, name, 2);
+  this.name = name;
+
+  Metrics.Measurement.call(this);
 }
+
 DummyMeasurement.prototype = {
-  __proto__: MetricsMeasurement.prototype,
+  __proto__: Metrics.Measurement.prototype,
+
+  version: 1,
 
-  fields: {
-    "string": {
-      type: "TYPE_STRING",
-    },
-
-    "uint32": {
-      type: "TYPE_UINT32",
-      optional: true,
-    },
+  configureStorage: function () {
+    let self = this;
+    return Task.spawn(function configureStorage() {
+      yield self.registerStorageField("daily-counter", self.storage.FIELD_DAILY_COUNTER);
+      yield self.registerStorageField("daily-discrete-numeric", self.storage.FIELD_DAILY_DISCRETE_NUMERIC);
+      yield self.registerStorageField("daily-discrete-text", self.storage.FIELD_DAILY_DISCRETE_TEXT);
+      yield self.registerStorageField("daily-last-numeric", self.storage.FIELD_DAILY_LAST_NUMERIC);
+      yield self.registerStorageField("daily-last-text", self.storage.FIELD_DAILY_LAST_TEXT);
+      yield self.registerStorageField("last-numeric", self.storage.FIELD_LAST_NUMERIC);
+      yield self.registerStorageField("last-text", self.storage.FIELD_LAST_TEXT);
+    });
   },
 };
 
 
 this.DummyProvider = function DummyProvider(name="DummyProvider") {
-  MetricsProvider.call(this, name);
+  this.name = name;
+
+  this.measurementTypes = [DummyMeasurement];
+
+  Metrics.Provider.call(this);
 
   this.constantMeasurementName = "DummyMeasurement";
   this.collectConstantCount = 0;
-  this.throwDuringCollectConstantMeasurements = null;
+  this.throwDuringCollectConstantData = null;
   this.throwDuringConstantPopulate = null;
+
+  this.havePushedMeasurements = true;
 }
+
 DummyProvider.prototype = {
-  __proto__: MetricsProvider.prototype,
+  __proto__: Metrics.Provider.prototype,
 
-  collectConstantMeasurements: function collectConstantMeasurements() {
+  collectConstantData: function () {
     this.collectConstantCount++;
 
-    let result = this.createResult();
-    result.expectMeasurement(this.constantMeasurementName);
-
-    result.populate = this._populateConstantResult.bind(this);
-
-    if (this.throwDuringCollectConstantMeasurements) {
-      throw new Error(this.throwDuringCollectConstantMeasurements);
+    if (this.throwDuringCollectConstantData) {
+      throw new Error(this.throwDuringCollectConstantData);
     }
 
-    return result;
+    return this.enqueueStorageOperation(function doStorage() {
+      if (this.throwDuringConstantPopulate) {
+        throw new Error(this.throwDuringConstantPopulate);
+      }
+
+      let m = this.getMeasurement("DummyMeasurement", 1);
+      let now = new Date();
+      m.incrementDailyCounter("daily-counter", now);
+      m.addDailyDiscreteNumeric("daily-discrete-numeric", 1, now);
+      m.addDailyDiscreteNumeric("daily-discrete-numeric", 2, now);
+      m.addDailyDiscreteText("daily-discrete-text", "foo", now);
+      m.addDailyDiscreteText("daily-discrete-text", "bar", now);
+      m.setDailyLastNumeric("daily-last-numeric", 3, now);
+      m.setDailyLastText("daily-last-text", "biz", now);
+      m.setLastNumeric("last-numeric", 4, now);
+      return m.setLastText("last-text", "bazfoo", now);
+    }.bind(this));
   },
 
-  _populateConstantResult: function _populateConstantResult(result) {
-    if (this.throwDuringConstantPopulate) {
-      throw new Error(this.throwDuringConstantPopulate);
-    }
+};
 
-    this._log.debug("Populating constant measurement in DummyProvider.");
-    result.addMeasurement(new DummyMeasurement(this.constantMeasurementName));
-
-    result.setValue(this.constantMeasurementName, "string", "foo");
-    result.setValue(this.constantMeasurementName, "uint32", 24);
-
-    result.finish();
-  },
-};
new file mode 100644
--- /dev/null
+++ b/services/metrics/storage.jsm
@@ -0,0 +1,2028 @@
+/* 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 = [
+  "MetricsStorageBackend",
+  "dateToDays",
+  "daysToDate",
+];
+
+const {utils: Cu} = Components;
+
+Cu.import("resource://gre/modules/commonjs/promise/core.js");
+Cu.import("resource://gre/modules/Sqlite.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+Cu.import("resource://services-common/log4moz.js");
+Cu.import("resource://services-common/utils.js");
+
+
+const MILLISECONDS_PER_DAY = 24 * 60 * 60 * 1000;
+
+// These do not account for leap seconds. Meh.
+function dateToDays(date) {
+  return Math.floor(date.getTime() / MILLISECONDS_PER_DAY);
+}
+
+function daysToDate(days) {
+  return new Date(days * MILLISECONDS_PER_DAY);
+}
+
+/**
+ * Represents a collection of per-day values.
+ *
+ * This is a proxy around a Map which can transparently round Date instances to
+ * their appropriate key.
+ *
+ * This emulates Map by providing .size and iterator support. Note that keys
+ * from the iterator are Date instances corresponding to midnight of the start
+ * of the day. get(), has(), and set() are modeled as getDay(), hasDay(), and
+ * setDay(), respectively.
+ *
+ * All days are defined in terms of UTC (as opposed to local time).
+ */
+function DailyValues() {
+  this._days = new Map();
+}
+
+DailyValues.prototype = Object.freeze({
+  __iterator__: function () {
+    for (let [k, v] of this._days) {
+      yield [daysToDate(k), v];
+    }
+  },
+
+  get size() {
+    return this._days.size;
+  },
+
+  hasDay: function (date) {
+    return this._days.has(dateToDays(date));
+  },
+
+  getDay: function (date) {
+    return this._days.get(dateToDays(date));
+  },
+
+  setDay: function (date, value) {
+    this._days.set(dateToDays(date), value);
+  },
+
+  appendValue: function (date, value) {
+    let key = dateToDays(date);
+
+    if (this._days.has(key)) {
+      return this._days.get(key).push(value);
+    }
+
+    this._days.set(key, [value]);
+  },
+});
+
+
+/**
+ * DATABASE INFO
+ * =============
+ *
+ * We use a SQLite database as the backend for persistent storage of metrics
+ * data.
+ *
+ * Every piece of recorded data is associated with a measurement. A measurement
+ * is an entity with a name and version. Each measurement is associated with a
+ * named provider.
+ *
+ * When the metrics system is initialized, we ask providers (the entities that
+ * emit data) to configure the database for storage of their data. They tell
+ * storage what their requirements are. For example, they'll register
+ * named daily counters associated with specific measurements.
+ *
+ * Recorded data is stored differently depending on the requirements for
+ * storing it. We have facilities for storing the following classes of data:
+ *
+ *  1) Counts of event/field occurrences aggregated by day.
+ *  2) Discrete values of fields aggregated by day.
+ *  3) Discrete values of fields aggregated by day max 1 per day (last write
+ *     wins).
+ *  4) Discrete values of fields max 1 (last write wins).
+ *
+ * Most data is aggregated per day mainly for privacy reasons. This does throw
+ * away potentially useful data. But, it's not currently used, so there is no
+ * need to keep the granular information.
+ *
+ * Database Schema
+ * ---------------
+ *
+ * This database contains the following tables:
+ *
+ *   providers -- Maps provider string name to an internal ID.
+ *   provider_state -- Holds opaque persisted state for providers.
+ *   measurements -- Holds the set of known measurements (name, version,
+ *     provider tuples).
+ *   types -- The data types that can be stored in measurements/fields.
+ *   fields -- Describes entities that occur within measurements.
+ *   daily_counters -- Holds daily-aggregated counts of events. Each row is
+ *     associated with a field and a day.
+ *   daily_discrete_numeric -- Holds numeric values for fields grouped by day.
+ *     Each row contains a discrete value associated with a field that occurred
+ *     on a specific day. There can be multiple rows per field per day.
+ *   daily_discrete_text -- Holds text values for fields grouped by day. Each
+ *     row contains a discrete value associated with a field that occurred on a
+ *     specific day.
+ *   daily_last_numeric -- Holds numeric values where the last encountered
+ *     value for a given day is retained.
+ *   daily_last_text -- Like daily_last_numeric except for text values.
+ *   last_numeric -- Holds the most recent value for a numeric field.
+ *   last_text -- Like last_numeric except for text fields.
+ *
+ * Notes
+ * -----
+ *
+ * It is tempting to use SQLite's julianday() function to store days that
+ * things happened. However, a Julian Day begins at *noon* in 4714 B.C. This
+ * results in weird half day offsets from UNIX time. So, we instead store
+ * number of days since UNIX epoch, not Julian.
+ */
+
+/**
+ * All of our SQL statements are stored in a central mapping so they can easily
+ * be audited for security, perf, etc.
+ */
+const SQL = {
+  // Create the providers table.
+  createProvidersTable:
+    "CREATE TABLE providers (" +
+      "id INTEGER PRIMARY KEY AUTOINCREMENT, " +
+      "name TEXT, " +
+      "UNIQUE (name) " +
+    ")",
+
+  createProviderStateTable:
+    "CREATE TABLE provider_state (" +
+      "id INTEGER PRIMARY KEY AUTOINCREMENT, " +
+      "provider_id INTEGER, " +
+      "name TEXT, " +
+      "VALUE TEXT, " +
+      "UNIQUE (provider_id, name), " +
+      "FOREIGN KEY (provider_id) REFERENCES providers(id) ON DELETE CASCADE" +
+    ")",
+
+  createProviderStateProviderIndex:
+    "CREATE INDEX i_provider_state_provider_id ON provider_state (provider_id)",
+
+  createMeasurementsTable:
+    "CREATE TABLE measurements (" +
+      "id INTEGER PRIMARY KEY AUTOINCREMENT, " +
+      "provider_id INTEGER, " +
+      "name TEXT, " +
+      "version INTEGER, " +
+      "UNIQUE (provider_id, name, version), " +
+      "FOREIGN KEY (provider_id) REFERENCES providers(id) ON DELETE CASCADE" +
+    ")",
+
+  createMeasurementsProviderIndex:
+    "CREATE INDEX i_measurements_provider_id ON measurements (provider_id)",
+
+  createMeasurementsView:
+    "CREATE VIEW v_measurements AS " +
+      "SELECT " +
+        "providers.id AS provider_id, " +
+        "providers.name AS provider_name, " +
+        "measurements.id AS measurement_id, " +
+        "measurements.name AS measurement_name, " +
+        "measurements.version AS measurement_version " +
+      "FROM providers, measurements " +
+      "WHERE " +
+        "measurements.provider_id = providers.id",
+
+  createTypesTable:
+    "CREATE TABLE types (" +
+      "id INTEGER PRIMARY KEY AUTOINCREMENT, " +
+      "name TEXT, " +
+      "UNIQUE (name)" +
+    ")",
+
+  createFieldsTable:
+    "CREATE TABLE fields (" +
+      "id INTEGER PRIMARY KEY AUTOINCREMENT, " +
+      "measurement_id INTEGER, " +
+      "name TEXT, " +
+      "value_type INTEGER , " +
+      "UNIQUE (measurement_id, name), " +
+      "FOREIGN KEY (measurement_id) REFERENCES measurements(id) ON DELETE CASCADE " +
+      "FOREIGN KEY (value_type) REFERENCES types(id) ON DELETE CASCADE " +
+    ")",
+
+  createFieldsMeasurementIndex:
+    "CREATE INDEX i_fields_measurement_id ON fields (measurement_id)",
+
+  createFieldsView:
+    "CREATE VIEW v_fields AS " +
+      "SELECT " +
+        "providers.id AS provider_id, " +
+        "providers.name AS provider_name, " +
+        "measurements.id AS measurement_id, " +
+        "measurements.name AS measurement_name, " +
+        "measurements.version AS measurement_version, " +
+        "fields.id AS field_id, " +
+        "fields.name AS field_name, " +
+        "types.id AS type_id, " +
+        "types.name AS type_name " +
+      "FROM providers, measurements, fields, types " +
+      "WHERE " +
+        "fields.measurement_id = measurements.id " +
+        "AND measurements.provider_id = providers.id " +
+        "AND fields.value_type = types.id",
+
+  createDailyCountersTable:
+    "CREATE TABLE daily_counters (" +
+      "field_id INTEGER, " +
+      "day INTEGER, " +
+      "value INTEGER, " +
+      "UNIQUE(field_id, day), " +
+      "FOREIGN KEY (field_id) REFERENCES fields(id) ON DELETE CASCADE" +
+    ")",
+
+  createDailyCountersFieldIndex:
+    "CREATE INDEX i_daily_counters_field_id ON daily_counters (field_id)",
+
+  createDailyCountersDayIndex:
+    "CREATE INDEX i_daily_counters_day ON daily_counters (day)",
+
+  createDailyCountersView:
+    "CREATE VIEW v_daily_counters AS SELECT " +
+      "providers.id AS provider_id, " +
+      "providers.name AS provider_name, " +
+      "measurements.id AS measurement_id, " +
+      "measurements.name AS measurement_name, " +
+      "measurements.version AS measurement_version, " +
+      "fields.id AS field_id, " +
+      "fields.name AS field_name, " +
+      "daily_counters.day AS day, " +
+      "daily_counters.value AS value " +
+    "FROM providers, measurements, fields, daily_counters " +
+    "WHERE " +
+      "daily_counters.field_id = fields.id " +
+      "AND fields.measurement_id = measurements.id " +
+      "AND measurements.provider_id = providers.id",
+
+  createDailyDiscreteNumericsTable:
+    "CREATE TABLE daily_discrete_numeric (" +
+      "id INTEGER PRIMARY KEY AUTOINCREMENT, " +
+      "field_id INTEGER, " +
+      "day INTEGER, " +
+      "value INTEGER, " +
+      "FOREIGN KEY (field_id) REFERENCES fields(id) ON DELETE CASCADE" +
+    ")",
+
+  createDailyDiscreteNumericsFieldIndex:
+    "CREATE INDEX i_daily_discrete_numeric_field_id " +
+    "ON daily_discrete_numeric (field_id)",
+
+  createDailyDiscreteNumericsDayIndex:
+    "CREATE INDEX i_daily_discrete_numeric_day " +
+    "ON daily_discrete_numeric (day)",
+
+  createDailyDiscreteTextTable:
+    "CREATE TABLE daily_discrete_text (" +
+      "id INTEGER PRIMARY KEY AUTOINCREMENT, " +
+      "field_id INTEGER, " +
+      "day INTEGER, " +
+      "value TEXT, " +
+      "FOREIGN KEY (field_id) REFERENCES fields(id) ON DELETE CASCADE" +
+    ")",
+
+  createDailyDiscreteTextFieldIndex:
+    "CREATE INDEX i_daily_discrete_text_field_id " +
+    "ON daily_discrete_text (field_id)",
+
+  createDailyDiscreteTextDayIndex:
+    "CREATE INDEX i_daily_discrete_text_day " +
+    "ON daily_discrete_text (day)",
+
+  createDailyDiscreteView:
+    "CREATE VIEW v_daily_discrete AS " +
+      "SELECT " +
+        "providers.id AS provider_id, " +
+        "providers.name AS provider_name, " +
+        "measurements.id AS measurement_id, " +
+        "measurements.name AS measurement_name, " +
+        "measurements.version AS measurement_version, " +
+        "fields.id AS field_id, " +
+        "fields.name AS field_name, " +
+        "daily_discrete_numeric.id AS value_id, " +
+        "daily_discrete_numeric.day AS day, " +
+        "daily_discrete_numeric.value AS value, " +
+        '"numeric" AS value_type ' +
+        "FROM providers, measurements, fields, daily_discrete_numeric " +
+        "WHERE " +
+          "daily_discrete_numeric.field_id = fields.id " +
+          "AND fields.measurement_id = measurements.id " +
+          "AND measurements.provider_id = providers.id " +
+      "UNION ALL " +
+      "SELECT " +
+        "providers.id AS provider_id, " +
+        "providers.name AS provider_name, " +
+        "measurements.id AS measurement_id, " +
+        "measurements.name AS measurement_name, " +
+        "measurements.version AS measurement_version, " +
+        "fields.id AS field_id, " +
+        "fields.name AS field_name, " +
+        "daily_discrete_text.id AS value_id, " +
+        "daily_discrete_text.day AS day, " +
+        "daily_discrete_text.value AS value, " +
+        '"text" AS value_type ' +
+        "FROM providers, measurements, fields, daily_discrete_text " +
+        "WHERE " +
+          "daily_discrete_text.field_id = fields.id " +
+          "AND fields.measurement_id = measurements.id " +
+          "AND measurements.provider_id = providers.id " +
+      "ORDER BY day ASC, value_id ASC",
+
+  createLastNumericTable:
+    "CREATE TABLE last_numeric (" +
+      "field_id INTEGER PRIMARY KEY, " +
+      "day INTEGER, " +
+      "value NUMERIC, " +
+      "FOREIGN KEY (field_id) REFERENCES fields(id) ON DELETE CASCADE" +
+    ")",
+
+  createLastTextTable:
+    "CREATE TABLE last_text (" +
+      "field_id INTEGER PRIMARY KEY, " +
+      "day INTEGER, " +
+      "value TEXT, " +
+      "FOREIGN KEY (field_id) REFERENCES fields(id) ON DELETE CASCADE" +
+    ")",
+
+  createLastView:
+    "CREATE VIEW v_last AS " +
+      "SELECT " +
+        "providers.id AS provider_id, " +
+        "providers.name AS provider_name, " +
+        "measurements.id AS measurement_id, " +
+        "measurements.name AS measurement_name, " +
+        "measurements.version AS measurement_version, " +
+        "fields.id AS field_id, " +
+        "fields.name AS field_name, " +
+        "last_numeric.day AS day, " +
+        "last_numeric.value AS value, " +
+        '"numeric" AS value_type ' +
+        "FROM providers, measurements, fields, last_numeric " +
+        "WHERE " +
+          "last_numeric.field_id = fields.id " +
+          "AND fields.measurement_id = measurements.id " +
+          "AND measurements.provider_id = providers.id " +
+      "UNION ALL " +
+      "SELECT " +
+        "providers.id AS provider_id, " +
+        "providers.name AS provider_name, " +
+        "measurements.id AS measurement_id, " +
+        "measurements.name AS measurement_name, " +
+        "measurements.version AS measurement_version, " +
+        "fields.id AS field_id, " +
+        "fields.name AS field_name, " +
+        "last_text.day AS day, " +
+        "last_text.value AS value, " +
+        '"text" AS value_type ' +
+        "FROM providers, measurements, fields, last_text " +
+        "WHERE " +
+          "last_text.field_id = fields.id " +
+          "AND fields.measurement_id = measurements.id " +
+          "AND measurements.provider_id = providers.id",
+
+  createDailyLastNumericTable:
+    "CREATE TABLE daily_last_numeric (" +
+      "field_id INTEGER, " +
+      "day INTEGER, " +
+      "value NUMERIC, " +
+      "UNIQUE (field_id, day) " +
+      "FOREIGN KEY (field_id) REFERENCES fields(id) ON DELETE CASCADE" +
+    ")",
+
+  createDailyLastNumericFieldIndex:
+    "CREATE INDEX i_daily_last_numeric_field_id ON daily_last_numeric (field_id)",
+
+  createDailyLastNumericDayIndex:
+    "CREATE INDEX i_daily_last_numeric_day ON daily_last_numeric (day)",
+
+  createDailyLastTextTable:
+    "CREATE TABLE daily_last_text (" +
+      "field_id INTEGER, " +
+      "day INTEGER, " +
+      "value TEXT, " +
+      "UNIQUE (field_id, day) " +
+      "FOREIGN KEY (field_id) REFERENCES fields(id) ON DELETE CASCADE" +
+    ")",
+
+  createDailyLastTextFieldIndex:
+    "CREATE INDEX i_daily_last_text_field_id ON daily_last_text (field_id)",
+
+  createDailyLastTextDayIndex:
+    "CREATE INDEX i_daily_last_text_day ON daily_last_text (day)",
+
+  createDailyLastView:
+    "CREATE VIEW v_daily_last AS " +
+      "SELECT " +
+        "providers.id AS provider_id, " +
+        "providers.name AS provider_name, " +
+        "measurements.id AS measurement_id, " +
+        "measurements.name AS measurement_name, " +
+        "measurements.version AS measurement_version, " +
+        "fields.id AS field_id, " +
+        "fields.name AS field_name, " +
+        "daily_last_numeric.day AS day, " +
+        "daily_last_numeric.value AS value, " +
+        '"numeric" as value_type ' +
+        "FROM providers, measurements, fields, daily_last_numeric " +
+        "WHERE " +
+          "daily_last_numeric.field_id = fields.id " +
+          "AND fields.measurement_id = measurements.id " +
+          "AND measurements.provider_id = providers.id " +
+      "UNION ALL " +
+      "SELECT " +
+        "providers.id AS provider_id, " +
+        "providers.name AS provider_name, " +
+        "measurements.id AS measurement_id, " +
+        "measurements.name AS measurement_name, " +
+        "measurements.version AS measurement_version, " +
+        "fields.id AS field_id, " +
+        "fields.name AS field_name, " +
+        "daily_last_text.day AS day, " +
+        "daily_last_text.value AS value, " +
+        '"text" as value_type ' +
+        "FROM providers, measurements, fields, daily_last_text " +
+        "WHERE " +
+          "daily_last_text.field_id = fields.id " +
+          "AND fields.measurement_id = measurements.id " +
+          "AND measurements.provider_id = providers.id",
+
+  // Mutation.
+
+  addProvider: "INSERT INTO providers (name) VALUES (:provider)",
+
+  setProviderState:
+    "INSERT OR REPLACE INTO provider_state " +
+      "(provider_id, name, value) " +
+      "VALUES (:provider_id, :name, :value)",
+
+  addMeasurement:
+    "INSERT INTO measurements (provider_id, name, version) " +
+      "VALUES (:provider_id, :measurement, :version)",
+
+  addType: "INSERT INTO types (name) VALUES (:name)",
+
+  addField:
+    "INSERT INTO fields (measurement_id, name, value_type) " +
+      "VALUES (:measurement_id, :field, :value_type)",
+
+  incrementDailyCounterFromFieldID:
+    "INSERT OR REPLACE INTO daily_counters VALUES (" +
+      ":field_id, " +
+      ":days, " +
+      "COALESCE(" +
+        "(SELECT value FROM daily_counters WHERE " +
+          "field_id = :field_id AND day = :days " +
+        "), " +
+        "0" +
+      ") + 1)",
+
+  deleteLastNumericFromFieldID:
+    "DELETE FROM last_numeric WHERE field_id = :field_id",
+
+  deleteLastTextFromFieldID:
+    "DELETE FROM last_text WHERE field_id = :field_id",
+
+  setLastNumeric:
+    "INSERT OR REPLACE INTO last_numeric VALUES (:field_id, :days, :value)",
+
+  setLastText:
+    "INSERT OR REPLACE INTO last_text VALUES (:field_id, :days, :value)",
+
+  setDailyLastNumeric:
+    "INSERT OR REPLACE INTO daily_last_numeric VALUES (:field_id, :days, :value)",
+
+  setDailyLastText:
+    "INSERT OR REPLACE INTO daily_last_text VALUES (:field_id, :days, :value)",
+
+  addDailyDiscreteNumeric:
+    "INSERT INTO daily_discrete_numeric " +
+    "(field_id, day, value) VALUES (:field_id, :days, :value)",
+
+  addDailyDiscreteText:
+    "INSERT INTO daily_discrete_text " +
+    "(field_id, day, value) VALUES (:field_id, :days, :value)",
+
+  pruneOldDailyCounters: "DELETE FROM daily_counters WHERE day < :days",
+  pruneOldDailyDiscreteNumeric: "DELETE FROM daily_discrete_numeric WHERE day < :days",
+  pruneOldDailyDiscreteText: "DELETE FROM daily_discrete_text WHERE day < :days",
+  pruneOldDailyLastNumeric: "DELETE FROM daily_last_numeric WHERE day < :days",
+  pruneOldDailyLastText: "DELETE FROM daily_last_text WHERE day < :days",
+  pruneOldLastNumeric: "DELETE FROM last_numeric WHERE day < :days",
+  pruneOldLastText: "DELETE FROM last_text WHERE day < :days",
+
+  // Retrieval.
+
+  getProviderID: "SELECT id FROM providers WHERE name = :provider",
+
+  getProviders: "SELECT id, name FROM providers",
+
+  getProviderStateWithName:
+    "SELECT value FROM provider_state " +
+      "WHERE provider_id = :provider_id " +
+      "AND name = :name",
+
+  getMeasurements: "SELECT * FROM v_measurements",
+
+  getMeasurementID:
+    "SELECT id FROM measurements " +
+      "WHERE provider_id = :provider_id " +
+        "AND name = :measurement " +
+        "AND version = :version",
+
+  getFieldID:
+    "SELECT id FROM fields " +
+      "WHERE measurement_id = :measurement_id " +
+        "AND name = :field " +
+        "AND value_type = :value_type " +
+    "",
+
+  getTypes: "SELECT * FROM types",
+
+  getTypeID: "SELECT id FROM types WHERE name = :name",
+
+  getDailyCounterCountsFromFieldID:
+    "SELECT day, value FROM daily_counters " +
+      "WHERE field_id = :field_id " +
+      "ORDER BY day ASC",
+
+  getDailyCounterCountFromFieldID:
+    "SELECT value FROM daily_counters " +
+      "WHERE field_id = :field_id " +
+        "AND day = :days",
+
+  getMeasurementDailyCounters:
+    "SELECT field_name, day, value FROM v_daily_counters " +
+    "WHERE measurement_id = :measurement_id",
+
+  getFieldInfo: "SELECT * FROM v_fields",
+
+  getLastNumericFromFieldID:
+    "SELECT day, value FROM last_numeric WHERE field_id = :field_id",
+
+  getLastTextFromFieldID:
+    "SELECT day, value FROM last_text WHERE field_id = :field_id",
+
+  getMeasurementLastValues:
+    "SELECT field_name, day, value FROM v_last " +
+    "WHERE measurement_id = :measurement_id",
+
+  getDailyDiscreteNumericFromFieldID:
+    "SELECT day, value FROM daily_discrete_numeric " +
+      "WHERE field_id = :field_id " +
+      "ORDER BY day ASC, id ASC",
+
+  getDailyDiscreteNumericFromFieldIDAndDay:
+    "SELECT day, value FROM daily_discrete_numeric " +
+      "WHERE field_id = :field_id AND day = :days " +
+      "ORDER BY id ASC",
+
+  getDailyDiscreteTextFromFieldID:
+    "SELECT day, value FROM daily_discrete_text " +
+      "WHERE field_id = :field_id " +
+      "ORDER BY day ASC, id ASC",
+
+  getDailyDiscreteTextFromFieldIDAndDay:
+    "SELECT day, value FROM daily_discrete_text " +
+      "WHERE field_id = :field_id AND day = :days " +
+      "ORDER BY id ASC",
+
+  getMeasurementDailyDiscreteValues:
+    "SELECT field_name, day, value_id, value FROM v_daily_discrete " +
+    "WHERE measurement_id = :measurement_id " +
+    "ORDER BY day ASC, value_id ASC",
+
+  getDailyLastNumericFromFieldID:
+    "SELECT day, value FROM daily_last_numeric " +
+      "WHERE field_id = :field_id " +
+      "ORDER BY day ASC",
+
+  getDailyLastNumericFromFieldIDAndDay:
+    "SELECT day, value FROM daily_last_numeric " +
+      "WHERE field_id = :field_id AND day = :days",
+
+  getDailyLastTextFromFieldID:
+    "SELECT day, value FROM daily_last_text " +
+      "WHERE field_id = :field_id " +
+      "ORDER BY day ASC",
+
+  getDailyLastTextFromFieldIDAndDay:
+    "SELECT day, value FROM daily_last_text " +
+      "WHERE field_id = :field_id AND day = :days",
+
+  getMeasurementDailyLastValues:
+    "SELECT field_name, day, value FROM v_daily_last " +
+    "WHERE measurement_id = :measurement_id",
+};
+
+
+function dailyKeyFromDate(date) {
+  let year = String(date.getUTCFullYear());
+  let month = String(date.getUTCMonth() + 1);
+  let day = String(date.getUTCDate());
+
+  if (month.length < 2) {
+    month = "0" + month;
+  }
+
+  if (day.length < 2) {
+    day = "0" + day;
+  }
+
+  return year + "-" + month + "-" + day;
+}
+
+
+/**
+ * Create a new backend instance bound to a SQLite database at the given path.
+ *
+ * This returns a promise that will resolve to a `MetricsStorageSqliteBackend`
+ * instance. The resolved instance will be initialized and ready for use.
+ *
+ * Very few consumers have a need to call this. Instead, a higher-level entity
+ * likely calls this and sets up the database connection for a service or
+ * singleton.
+ */
+this.MetricsStorageBackend = function (path) {
+  return Task.spawn(function initTask() {
+    let connection = yield Sqlite.openConnection({
+      path: path,
+
+      // There should only be one connection per database, so we disable this
+      // for perf reasons.
+      sharedMemoryCache: false,
+    });
+
+    // If we fail initializing the storage object, we need to close the
+    // database connection or else Storage will assert on shutdown.
+    let storage;
+    try {
+      storage = new MetricsStorageSqliteBackend(connection);
+      yield storage._init();
+    } catch (ex) {
+      yield connection.close();
+      throw ex;
+    }
+
+    throw new Task.Result(storage);
+  });
+};
+
+
+/**
+ * Manages storage of metrics data in a SQLite database.
+ *
+ * This is the main type used for interfacing with the database.
+ *
+ * Instances of this should be obtained by calling MetricsStorageConnection().
+ *
+ * The current implementation will not work if the database is mutated by
+ * multiple connections because of the way we cache primary keys.
+ *
+ * FUTURE enforce 1 read/write connection per database limit.
+ */
+function MetricsStorageSqliteBackend(connection) {
+  this._log = Log4Moz.repository.getLogger("Services.Metrics.MetricsStorage");
+
+  this._connection = connection;
+
+  // Integer IDs to string name.
+  this._typesByID = new Map();
+
+  // String name to integer IDs.
+  this._typesByName = new Map();
+
+  // Maps provider names to integer IDs.
+  this._providerIDs = new Map();
+
+  // Maps :-delimited strings of [provider name, name, version] to integer IDs.
+  this._measurementsByInfo = new Map();
+
+  // Integer IDs to Arrays of [provider name, name, version].
+  this._measurementsByID = new Map();
+
+  // Integer IDs to Arrays of [measurement id, field name, value name]
+  this._fieldsByID = new Map();
+
+  // Maps :-delimited strings of [measurement id, field name] to integer ID.
+  this._fieldsByInfo = new Map();
+
+  // Maps measurement ID to sets of field IDs.
+  this._fieldsByMeasurement = new Map();
+
+  this._queuedOperations = [];
+  this._queuedInProgress = false;
+}
+
+MetricsStorageSqliteBackend.prototype = Object.freeze({
+  FIELD_DAILY_COUNTER: "daily-counter",
+  FIELD_DAILY_DISCRETE_NUMERIC: "daily-discrete-numeric",
+  FIELD_DAILY_DISCRETE_TEXT: "daily-discrete-text",
+  FIELD_DAILY_LAST_NUMERIC: "daily-last-numeric",
+  FIELD_DAILY_LAST_TEXT: "daily-last-text",
+  FIELD_LAST_NUMERIC: "last-numeric",
+  FIELD_LAST_TEXT: "last-text",
+
+  _BUILTIN_TYPES: [
+    "FIELD_DAILY_COUNTER",
+    "FIELD_DAILY_DISCRETE_NUMERIC",
+    "FIELD_DAILY_DISCRETE_TEXT",
+    "FIELD_DAILY_LAST_NUMERIC",
+    "FIELD_DAILY_LAST_TEXT",
+    "FIELD_LAST_NUMERIC",
+    "FIELD_LAST_TEXT",
+  ],
+
+  // Statements that are used to create the initial DB schema.
+  _SCHEMA_STATEMENTS: [
+    "createProvidersTable",
+    "createProviderStateTable",
+    "createProviderStateProviderIndex",
+    "createMeasurementsTable",
+    "createMeasurementsProviderIndex",
+    "createMeasurementsView",
+    "createTypesTable",
+    "createFieldsTable",
+    "createFieldsMeasurementIndex",
+    "createFieldsView",
+    "createDailyCountersTable",
+    "createDailyCountersFieldIndex",
+    "createDailyCountersDayIndex",
+    "createDailyCountersView",
+    "createDailyDiscreteNumericsTable",
+    "createDailyDiscreteNumericsFieldIndex",
+    "createDailyDiscreteNumericsDayIndex",
+    "createDailyDiscreteTextTable",
+    "createDailyDiscreteTextFieldIndex",
+    "createDailyDiscreteTextDayIndex",
+    "createDailyDiscreteView",
+    "createDailyLastNumericTable",
+    "createDailyLastNumericFieldIndex",
+    "createDailyLastNumericDayIndex",
+    "createDailyLastTextTable",
+    "createDailyLastTextFieldIndex",
+    "createDailyLastTextDayIndex",
+    "createDailyLastView",
+    "createLastNumericTable",
+    "createLastTextTable",
+    "createLastView",
+  ],
+
+  // Statements that are used to prune old data.
+  _PRUNE_STATEMENTS: [
+    "pruneOldDailyCounters",
+    "pruneOldDailyDiscreteNumeric",
+    "pruneOldDailyDiscreteText",
+    "pruneOldDailyLastNumeric",
+    "pruneOldDailyLastText",
+    "pruneOldLastNumeric",
+    "pruneOldLastText",
+  ],
+
+  /**
+   * Close the database connection.
+   *
+   * This should be called on all instances or the SQLite layer may complain
+   * loudly. After this has been called, the connection cannot be used.
+   *
+   * @return Promise<>
+   */
+  close: function () {
+    return Task.spawn(function doClose() {
+      // There is some light magic involved here. First, we enqueue an
+      // operation to ensure that all pending operations have the opportunity
+      // to execute. We additionally execute a SQL operation. Due to the FIFO
+      // execution order of issued statements, this will cause us to wait on
+      // any outstanding statements before closing.
+      try {
+        yield this.enqueueOperation(function dummyOperation() {
+          return this._connection.execute("SELECT 1");
+        }.bind(this));
+      } catch (ex) {}
+
+      try {
+        yield this._connection.close();
+      } finally {
+        this._connection = null;
+      }
+    }.bind(this));
+  },
+
+  /**
+   * Whether a provider is known to exist.
+   *
+   * @param provider
+   *        (string) Name of the provider.
+   */
+  hasProvider: function (provider) {
+    return this._providerIDs.has(provider);
+  },
+
+  /**
+   * Whether a measurement is known to exist.
+   *
+   * @param provider
+   *        (string) Name of the provider.
+   * @param name
+   *        (string) Name of the measurement.
+   * @param version
+   *        (Number) Integer measurement version.
+   */
+  hasMeasurement: function (provider, name, version) {
+    return this._measurementsByInfo.has([provider, name, version].join(":"));
+  },
+
+  /**
+   * Whether a named field exists in a measurement.
+   *
+   * @param measurementID
+   *        (Number) The integer primary key of the measurement.
+   * @param field
+   *        (string) The name of the field to look for.
+   */
+  hasFieldFromMeasurement: function (measurementID, field) {
+    return this._fieldsByInfo.has([measurementID, field].join(":"));
+  },
+
+  /**
+   * Whether a field is known.
+   *
+   * @param provider
+   *        (string) Name of the provider having the field.
+   * @param measurement
+   *        (string) Name of the measurement in the provider having the field.
+   * @param field
+   *        (string) Name of the field in the measurement.
+   */
+  hasField: function (provider, measurement, version, field) {
+    let key = [provider, measurement, version].join(":");
+    let measurementID = this._measurementsByInfo.get(key);
+    if (!measurementID) {
+      return false;
+    }
+
+    return this.hasFieldFromMeasurement(measurementID, field);
+  },
+
+  /**
+   * Look up the integer primary key of a provider.
+   *
+   * @param provider
+   *        (string) Name of the provider.
+   */
+  providerID: function (provider) {
+    return this._providerIDs.get(provider);
+  },
+
+  /**
+   * Look up the integer primary key of a measurement.
+   *
+   * @param provider
+   *        (string) Name of the provider.
+   * @param measurement
+   *        (string) Name of the measurement.
+   * @param version
+   *        (Number) Integer version of the measurement.
+   */
+  measurementID: function (provider, measurement, version) {
+    return this._measurementsByInfo.get([provider, measurement, version].join(":"));
+  },
+
+  fieldIDFromMeasurement: function (measurementID, field) {
+    return this._fieldsByInfo.get([measurementID, field].join(":"));
+  },
+
+  fieldID: function (provider, measurement, version, field) {
+    let measurementID = this.measurementID(provider, measurement, version);
+    if (!measurementID) {
+      return null;
+    }
+
+    return this.fieldIDFromMeasurement(measurementID, field);
+  },
+
+  measurementHasAnyDailyCounterFields: function (measurementID) {
+    return this.measurementHasAnyFieldsOfTypes(measurementID,
+                                               [this.FIELD_DAILY_COUNTER]);
+  },
+
+  measurementHasAnyLastFields: function (measurementID) {
+    return this.measurementHasAnyFieldsOfTypes(measurementID,
+                                               [this.FIELD_LAST_NUMERIC,
+                                                this.FIELD_LAST_TEXT]);
+  },
+
+  measurementHasAnyDailyLastFields: function (measurementID) {
+    return this.measurementHasAnyFieldsOfTypes(measurementID,
+                                               [this.FIELD_DAILY_LAST_NUMERIC,
+                                                this.FIELD_DAILY_LAST_TEXT]);
+  },
+
+  measurementHasAnyDailyDiscreteFields: function (measurementID) {
+    return this.measurementHasAnyFieldsOfTypes(measurementID,
+                                               [this.FIELD_DAILY_DISCRETE_NUMERIC,
+                                                this.FIELD_DAILY_DISCRETE_TEXT]);
+  },
+
+  measurementHasAnyFieldsOfTypes: function (measurementID, types) {
+    if (!this._fieldsByMeasurement.has(measurementID)) {
+      return false;
+    }
+
+    let fieldIDs = this._fieldsByMeasurement.get(measurementID);
+    for (let fieldID of fieldIDs) {
+      let fieldType = this._fieldsByID.get(fieldID)[2];
+      if (types.indexOf(fieldType) != -1) {
+        return true;
+      }
+    }
+
+    return false;
+  },
+
+  /**
+   * Register a measurement with the backend.
+   *
+   * Measurements must be registered before storage can be allocated to them.
+   *
+   * A measurement consists of a string name and integer version attached
+   * to a named provider.
+   *
+   * This returns a promise that resolves to the storage ID for this
+   * measurement.
+   *
+   * If the measurement is not known to exist, it is registered with storage.
+   * If the measurement has already been registered, this is effectively a
+   * no-op (that still returns a promise resolving to the storage ID).
+   *
+   * @param provider
+   *        (string) Name of the provider this measurement belongs to.
+   * @param name
+   *        (string) Name of this measurement.
+   * @param version
+   *        (Number) Integer version of this measurement.
+   */
+  registerMeasurement: function (provider, name, version) {
+    if (this.hasMeasurement(provider, name, version)) {
+      return Promise.resolve(this.measurementID(provider, name, version));
+    }
+
+    // Registrations might not be safe to perform in parallel with provider
+    // operations. So, we queue them.
+    let self = this;
+    return this.enqueueOperation(function createMeasurementOperation() {
+      return Task.spawn(function createMeasurement() {
+        let providerID = self._providerIDs.get(provider);
+
+        if (!providerID) {
+          yield self._connection.executeCached(SQL.addProvider, {provider: provider});
+          let rows = yield self._connection.executeCached(SQL.getProviderID,
+                                                          {provider: provider});
+
+          providerID = rows[0].getResultByIndex(0);
+
+          self._providerIDs.set(provider, providerID);
+        }
+
+        let params = {
+          provider_id: providerID,
+          measurement: name,
+          version: version,
+        };
+
+        yield self._connection.executeCached(SQL.addMeasurement, params);
+        let rows = yield self._connection.executeCached(SQL.getMeasurementID, params);
+
+        let measurementID = rows[0].getResultByIndex(0);
+
+        self._measurementsByInfo.set([provider, name, version].join(":"), measurementID);
+        self._measurementsByID.set(measurementID, [provider, name, version]);
+        self._fieldsByMeasurement.set(measurementID, new Set());
+
+        throw new Task.Result(measurementID);
+      });
+    });
+  },
+
+  /**
+   * Register a field with the backend.
+   *
+   * Fields are what recorded pieces of data are primarily associated with.
+   *
+   * Fields are associated with measurements. Measurements must be registered
+   * via `registerMeasurement` before fields can be registered. This is
+   * enforced by this function requiring the database primary key of the
+   * measurement as an argument.
+   *
+   * @param measurementID
+   *        (Number) Integer primary key of measurement this field belongs to.
+   * @param field
+   *        (string) Name of this field.
+   * @param valueType
+   *        (string) Type name of this field. Must be a registered type. Is
+   *        likely one of the FIELD_ constants on this type.
+   *
+   * @return Promise<integer>
+   */
+  registerField: function (measurementID, field, valueType) {
+    if (!valueType) {
+      throw new Error("Value type must be defined.");
+    }
+
+    if (!this._measurementsByID.has(measurementID)) {
+      throw new Error("Measurement not known: " + measurementID);
+    }
+
+    if (!this._typesByName.has(valueType)) {
+      throw new Error("Unknown value type: " + valueType);
+    }
+
+    let typeID = this._typesByName.get(valueType);
+
+    if (!typeID) {
+      throw new Error("Undefined type: " + valueType);
+    }
+
+    if (this.hasFieldFromMeasurement(measurementID, field)) {
+      let id = this.fieldIDFromMeasurement(measurementID, field);
+      let existingType = this._fieldsByID.get(id)[2];
+
+      if (valueType != existingType) {
+        throw new Error("Field already defined with different type: " + existingType);
+      }
+
+      return Promise.resolve(this.fieldIDFromMeasurement(measurementID, field));
+    }
+
+    let self = this;
+    return this.enqueueOperation(function addFieldOperation() {
+      return Task.spawn(function createField() {
+        let params = {
+          measurement_id: measurementID,
+          field: field,
+          value_type: typeID,
+        };
+
+        yield self._connection.executeCached(SQL.addField, params);
+
+        let rows = yield self._connection.executeCached(SQL.getFieldID, params);
+
+        let fieldID = rows[0].getResultByIndex(0);
+
+        self._fieldsByID.set(fieldID, [measurementID, field, valueType]);
+        self._fieldsByInfo.set([measurementID, field].join(":"), fieldID);
+        self._fieldsByMeasurement.get(measurementID).add(fieldID);
+
+        throw new Task.Result(fieldID);
+      });
+    });
+  },
+
+  /**
+   * Initializes this instance with the database.
+   *
+   * This performs 2 major roles:
+   *
+   *   1) Set up database schema (creates tables).
+   *   2) Synchronize database with local instance.
+   */
+  _init: function() {
+    let self = this;
+    return Task.spawn(function initTask() {
+      // 1. Create the schema.
+      yield self._connection.executeTransaction(function ensureSchema(conn) {
+        let schema = conn.schemaVersion;
+
+        if (schema == 0) {
+          self._log.info("Creating database schema.");
+
+          for (let k of self._SCHEMA_STATEMENTS) {
+            yield self._connection.execute(SQL[k]);
+          }
+
+          self._connection.schemaVersion = 1;
+        } else if (schema != 1) {
+          throw new Error("Unknown database schema: " + schema);
+        } else {
+          self._log.debug("Database schema up to date.");
+        }
+      });
+
+      // 2. Retrieve existing types.
+      yield self._connection.execute(SQL.getTypes, null, function onRow(row) {
+        let id = row.getResultByName("id");
+        let name = row.getResultByName("name");
+
+        self._typesByID.set(id, name);
+        self._typesByName.set(name, id);
+      });
+
+      // 3. Populate built-in types with database.
+      for (let type of self._BUILTIN_TYPES) {
+        type = self[type];
+        if (self._typesByName.has(type)) {
+          continue;
+        }
+
+        let params = {name: type};
+        yield self._connection.executeCached(SQL.addType, params);
+        let rows = yield self._connection.executeCached(SQL.getTypeID, params);
+        let id = rows[0].getResultByIndex(0);
+
+        self._typesByID.set(id, type);
+        self._typesByName.set(type, id);
+      }
+
+      // 4. Obtain measurement info.
+      yield self._connection.execute(SQL.getMeasurements, null, function onRow(row) {
+        let providerID = row.getResultByName("provider_id");
+        let providerName = row.getResultByName("provider_name");
+        let measurementID = row.getResultByName("measurement_id");
+        let measurementName = row.getResultByName("measurement_name");
+        let measurementVersion = row.getResultByName("measurement_version");
+
+        self._providerIDs.set(providerName, providerID);
+
+        let info = [providerName, measurementName, measurementVersion].join(":");
+
+        self._measurementsByInfo.set(info, measurementID);
+        self._measurementsByID.set(measurementID, info);
+        self._fieldsByMeasurement.set(measurementID, new Set());
+      });
+
+      // 5. Obtain field info.
+      yield self._connection.execute(SQL.getFieldInfo, null, function onRow(row) {
+        let measurementID = row.getResultByName("measurement_id");
+        let fieldID = row.getResultByName("field_id");
+        let fieldName = row.getResultByName("field_name");
+        let typeName = row.getResultByName("type_name");
+
+        self._fieldsByID.set(fieldID, [measurementID, fieldName, typeName]);
+        self._fieldsByInfo.set([measurementID, fieldName].join(":"), fieldID);
+        self._fieldsByMeasurement.get(measurementID).add(fieldID);
+      });
+    });
+  },
+
+  /**
+   * Prune all data from earlier than the specified date.
+   *
+   * Data stored on days before the specified Date will be permanently
+   * deleted.
+   *
+   * This returns a promise that will be resolved when data has been deleted.
+   *
+   * @param date
+   *        (Date) Old data threshold.
+   * @return Promise<>
+   */
+  pruneDataBefore: function (date) {
+    let statements = this._PRUNE_STATEMENTS;
+
+    let self = this;
+    return this.enqueueOperation(function doPrune() {
+      return self._connection.executeTransaction(function prune(conn) {
+        let days = dateToDays(date);
+
+        let params = {days: days};
+        for (let name of statements) {
+          yield conn.execute(SQL[name], params);
+        }
+      });
+    });
+  },
+
+  /**
+   * Ensure a field ID matches a specified type.
+   *
+   * This is called internally as part of adding values to ensure that
+   * the type of a field matches the operation being performed.
+   */
+  _ensureFieldType: function (id, type) {
+    let info = this._fieldsByID.get(id);
+
+    if (!info || !Array.isArray(info)) {
+      throw new Error("Unknown field ID: " + id);
+    }
+
+    if (type != info[2]) {
+      throw new Error("Field type does not match the expected for this " +
+                      "operation. Actual: " + info[2] + "; Expected: " +
+                      type);
+    }
+  },
+
+  /**
+   * Enqueue a storage operation to be performed when the database is ready.
+   *
+   * The primary use case of this function is to prevent potentially
+   * conflicting storage operations from being performed in parallel. By
+   * calling this function, passed storage operations will be serially
+   * executed, avoiding potential order of operation issues.
+   *
+   * The passed argument is a function that will perform storage operations.
+   * The function should return a promise that will be resolved when all
+   * storage operations have been completed.
+   *
+   * The passed function may be executed immediately. If there are already
+   * queued operations, it will be appended to the queue and executed after all
+   * before it have finished.
+   *
+   * This function returns a promise that will be resolved or rejected with
+   * the same value that the function's promise was resolved or rejected with.
+   *
+   * @param func
+   *        (function) Function performing storage interactions.
+   * @return Promise<>
+   */
+  enqueueOperation: function (func) {
+    if (typeof(func) != "function") {
+      throw new Error("enqueueOperation expects a function. Got: " + typeof(func));
+    }
+
+    let deferred = Promise.defer();
+
+    this._queuedOperations.push([func, deferred]);
+
+    if (this._queuedOperations.length == 1) {
+      this._popAndPerformQueuedOperation();
+    }
+
+    return deferred.promise;
+  },
+
+  _popAndPerformQueuedOperation: function () {
+    if (!this._queuedOperations.length || this._queuedInProgress) {
+      return;
+    }
+
+    this._log.trace("Performing queued operation.");
+    let [func, deferred] = this._queuedOperations.pop();
+    let promise;
+
+    try {
+      this._queuedInProgress = true;
+      promise = func();
+    } catch (ex) {
+      this._log.warn("Queued operation threw during execution: " +
+                     CommonUtils.exceptionStr(ex));
+      this._queuedInProgress = false;
+      deferred.reject(ex);
+      this._popAndPerformQueuedOperation();
+      return;
+    }
+
+    if (!promise || typeof(promise.then) != "function") {
+      let msg = "Queued operation did not return a promise: " + func;
+      this._log.warn(msg);
+
+      this._queuedInProgress = false;
+      deferred.reject(new Error(msg));
+      this._popAndPerformQueuedOperation();
+      return;
+    }
+
+    promise.then(
+      function onSuccess(result) {
+        this._log.trace("Queued operation completed.");
+        this._queuedInProgress = false;
+        deferred.resolve(result);
+        this._popAndPerformQueuedOperation();
+      }.bind(this),
+      function onError(error) {
+        this._log.warn("Failure when performing queued operation: " +
+                       CommonUtils.exceptionStr(error));
+        this._queuedInProgress = false;
+        deferred.reject(error);
+        this._popAndPerformQueuedOperation();
+      }.bind(this)
+    );
+  },
+
+  /**
+   * Obtain all values associated with a measurement.
+   *
+   * This returns a promise that resolves to an object. The keys of the object
+   * are:
+   *
+   *   days -- DailyValues where the values are Maps of field name to data
+   *     structures. The data structures could be simple (string or number) or
+   *     Arrays if the field type allows multiple values per day.
+   *
+   *   singular -- Map of field names to values. This holds all fields that
+   *     don't have a temporal component.
+   *
+   * @param id
+   *        (Number) Primary key of measurement whose values to retrieve.
+   */
+  getMeasurementValues: function (id) {
+    let deferred = Promise.defer();
+    let days = new DailyValues();
+    let singular = new Map();
+
+    let self = this;
+    this.enqueueOperation(function enqueuedGetMeasurementValues() {
+      return Task.spawn(function fetchMeasurementValues() {
+        function handleResult(data) {
+          for (let [field, values] of data) {
+            for (let [day, value] of Iterator(values)) {
+              if (!days.hasDay(day)) {
+                days.setDay(day, new Map());
+              }
+
+              days.getDay(day).set(field, value);
+            }
+          }
+        }
+
+        if (self.measurementHasAnyDailyCounterFields(id)) {
+          let counters = yield self.getMeasurementDailyCountersFromMeasurementID(id);
+          handleResult(counters);
+        }
+
+        if (self.measurementHasAnyDailyLastFields(id)) {
+          let dailyLast = yield self.getMeasurementDailyLastValuesFromMeasurementID(id);
+          handleResult(dailyLast);
+        }
+
+        if (self.measurementHasAnyDailyDiscreteFields(id)) {
+          let dailyDiscrete = yield self.getMeasurementDailyDiscreteValuesFromMeasurementID(id);
+          handleResult(dailyDiscrete);
+        }
+
+        if (self.measurementHasAnyLastFields(id)) {
+          let last = yield self.getMeasurementLastValuesFromMeasurementID(id);
+
+          for (let [field, value] of last) {
+            singular.set(field, value);
+          }
+        }
+
+      });
+    }).then(function onSuccess() {
+      deferred.resolve({singular: singular, days: days});
+    }, function onError(error) {
+      deferred.reject(error);
+    });
+
+    return deferred.promise;
+  },
+
+  //---------------------------------------------------------------------------
+  // Low-level storage operations
+  //
+  // These will be performed immediately (or at least as soon as the underlying
+  // connection allows them to be.) It is recommended to call these from within
+  // a function added via `enqueueOperation()` or they may inadvertently be
+  // performed during another enqueued operation, which may be a transaction
+  // that is rolled back.
+  // ---------------------------------------------------------------------------
+
+  /**
+   * Set state for a provider.
+   *
+   * Providers have the ability to register persistent state with the backend.
+   * Persistent state doesn't expire. The format of the data is completely up
+   * to the provider beyond the requirement that values be UTF-8 strings.
+   *
+   * This returns a promise that will be resolved when the underlying database
+   * operation has completed.
+   *
+   * @param provider
+   *        (string) Name of the provider.
+   * @param key
+   *        (string) Key under which to store this state.
+   * @param value
+   *        (string) Value for this state.
+   * @return Promise<>
+   */
+  setProviderState: function (provider, key, value) {
+    if (typeof(key) != "string") {
+      throw new Error("State key must be a string. Got: " + key);
+    }
+
+    if (typeof(value) != "string") {
+      throw new Error("State value must be a string. Got: " + value);
+    }
+
+    let id = this.providerID(provider);
+    if (!id) {
+      throw new Error("Unknown provider: " + provider);
+    }
+
+    return this._connection.executeCached(SQL.setProviderState, {
+      provider_id: id,
+      name: key,
+      value: value,
+    });
+  },
+
+  /**
+   * Obtain named state for a provider.
+   *
+   *
+   * The returned promise will resolve to the state from the database or null
+   * if the key is not stored.
+   *
+   * @param provider
+   *        (string) The name of the provider whose state to obtain.
+   * @param key
+   *        (string) The state's key to retrieve.
+   *
+   * @return Promise<data>
+   */
+  getProviderState: function (provider, key) {
+    let id = this.providerID(provider);
+    if (!id) {
+      throw new Error("Unknown provider: " + provider);
+    }
+
+    let conn = this._connection;
+    return Task.spawn(function queryDB() {
+      let rows = yield conn.executeCached(SQL.getProviderStateWithName, {
+        provider_id: id,
+        name: key,
+      });
+
+      if (!rows.length) {
+        throw new Task.Result(null);
+      }
+
+      throw new Task.Result(rows[0].getResultByIndex(0));
+    });
+  },
+
+  /**
+   * Increment a daily counter from a numeric field id.
+   *
+   * @param id
+   *        (integer) Primary key of field to increment.
+   * @param date
+   *        (Date) When the increment occurred. This is typically "now" but can
+   *        be explicitly defined for events that occurred in the past.
+   */
+  incrementDailyCounterFromFieldID: function (id, date=new Date()) {
+    this._ensureFieldType(id, this.FIELD_DAILY_COUNTER);
+
+    let params = {
+      field_id: id,
+      days: dateToDays(date),
+    };
+
+    return this._connection.executeCached(SQL.incrementDailyCounterFromFieldID,
+                                          params);
+  },
+
+  /**
+   * Obtain all counts for a specific daily counter.
+   *
+   * @param id
+   *        (integer) The ID of the field being retrieved.
+   */
+  getDailyCounterCountsFromFieldID: function (id) {
+    this._ensureFieldType(id, this.FIELD_DAILY_COUNTER);
+
+    let self = this;
+    return Task.spawn(function fetchCounterDays() {
+      let rows = yield self._connection.executeCached(SQL.getDailyCounterCountsFromFieldID,
+                                                      {field_id: id});
+