Bug 1313326 - Part 1: Support recording events in child processes. r=dexter, r=froydnj
☠☠ backed out by 5a561476eed2 ☠ ☠
authorGeorg Fritzsche <georg.fritzsche@googlemail.com>
Tue, 14 Feb 2017 14:43:51 +0100
changeset 374358 a6db4cae99904cecfc03101a7007a147aca6b1fc
parent 374357 1bea491fae965da7d65cf345f67ca5fb870d4356
child 374359 223a0be483694b387c57f662a4639da28dab3a97
push id10863
push userjlorenzo@mozilla.com
push dateMon, 06 Mar 2017 23:02:23 +0000
treeherdermozilla-aurora@0931190cd725 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersdexter, froydnj
bugs1313326
milestone54.0a1
Bug 1313326 - Part 1: Support recording events in child processes. r=dexter, r=froydnj
browser/modules/test/browser_UsageTelemetry_content.js
browser/modules/test/browser_UsageTelemetry_content_aboutHome.js
browser/modules/test/browser_UsageTelemetry_searchbar.js
browser/modules/test/browser_UsageTelemetry_urlbar.js
dom/ipc/ContentParent.cpp
dom/ipc/ContentParent.h
dom/ipc/PContent.ipdl
gfx/ipc/GPUChild.cpp
gfx/ipc/GPUChild.h
gfx/ipc/PGPU.ipdl
toolkit/components/telemetry/Telemetry.h
toolkit/components/telemetry/TelemetryEvent.cpp
toolkit/components/telemetry/TelemetryEvent.h
toolkit/components/telemetry/TelemetrySession.jsm
toolkit/components/telemetry/ipc/TelemetryComms.h
toolkit/components/telemetry/ipc/TelemetryIPC.cpp
toolkit/components/telemetry/ipc/TelemetryIPC.h
toolkit/components/telemetry/ipc/TelemetryIPCAccumulator.cpp
toolkit/components/telemetry/ipc/TelemetryIPCAccumulator.h
toolkit/components/telemetry/tests/unit/test_ChildEvents.js
toolkit/components/telemetry/tests/unit/test_TelemetryEvents.js
toolkit/components/telemetry/tests/unit/xpcshell.ini
--- a/browser/modules/test/browser_UsageTelemetry_content.js
+++ b/browser/modules/test/browser_UsageTelemetry_content.js
@@ -76,17 +76,18 @@ add_task(function* test_context_menu() {
   Assert.equal(Object.keys(scalars[SCALAR_CONTEXT_MENU]).length, 1,
                "This search must only increment one entry in the scalar.");
 
   // Make sure SEARCH_COUNTS contains identical values.
   checkKeyedHistogram(search_hist, "other-MozSearch.contextmenu", 1);
 
   // Also check events.
   let events = Services.telemetry.snapshotBuiltinEvents(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN, false);
-  events = events.filter(e => e[1] == "navigation" && e[2] == "search");
+  Assert.ok("default" in events, "We should have recorded events in the parent process.");
+  events = events.default.filter(e => e[1] == "navigation" && e[2] == "search");
   checkEvents(events, [["navigation", "search", "contextmenu", null, {engine: "other-MozSearch"}]]);
 
   contextMenu.hidePopup();
   yield BrowserTestUtils.removeTab(gBrowser.selectedTab);
   yield BrowserTestUtils.removeTab(tab);
 });
 
 add_task(function* test_about_newtab() {
@@ -112,13 +113,14 @@ add_task(function* test_about_newtab() {
   Assert.equal(Object.keys(scalars[SCALAR_ABOUT_NEWTAB]).length, 1,
                "This search must only increment one entry in the scalar.");
 
   // Make sure SEARCH_COUNTS contains identical values.
   checkKeyedHistogram(search_hist, "other-MozSearch.newtab", 1);
 
   // Also check events.
   let events = Services.telemetry.snapshotBuiltinEvents(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN, false);
-  events = events.filter(e => e[1] == "navigation" && e[2] == "search");
+  Assert.ok("default" in events, "We should have recorded events in the parent process.");
+  events = events.default.filter(e => e[1] == "navigation" && e[2] == "search");
   checkEvents(events, [["navigation", "search", "about_newtab", "enter", {engine: "other-MozSearch"}]]);
 
   yield BrowserTestUtils.removeTab(tab);
 });
--- a/browser/modules/test/browser_UsageTelemetry_content_aboutHome.js
+++ b/browser/modules/test/browser_UsageTelemetry_content_aboutHome.js
@@ -76,13 +76,14 @@ add_task(function* test_abouthome_simple
   Assert.equal(Object.keys(scalars[SCALAR_ABOUT_HOME]).length, 1,
                "This search must only increment one entry in the scalar.");
 
   // Make sure SEARCH_COUNTS contains identical values.
   checkKeyedHistogram(search_hist, "other-MozSearch.abouthome", 1);
 
   // Also check events.
   let events = Services.telemetry.snapshotBuiltinEvents(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN, false);
-  events = events.filter(e => e[1] == "navigation" && e[2] == "search");
+  Assert.ok("default" in events, "We should have recorded events in the parent process.");
+  events = events.default.filter(e => e[1] == "navigation" && e[2] == "search");
   checkEvents(events, [["navigation", "search", "about_home", "enter", {engine: "other-MozSearch"}]]);
 
   yield BrowserTestUtils.removeTab(tab);
 });
--- a/browser/modules/test/browser_UsageTelemetry_searchbar.js
+++ b/browser/modules/test/browser_UsageTelemetry_searchbar.js
@@ -102,17 +102,18 @@ add_task(function* test_plainQuery() {
   Assert.equal(Object.keys(scalars[SCALAR_SEARCHBAR]).length, 1,
                "This search must only increment one entry in the scalar.");
 
   // Make sure SEARCH_COUNTS contains identical values.
   checkKeyedHistogram(search_hist, "other-MozSearch.searchbar", 1);
 
   // Also check events.
   let events = Services.telemetry.snapshotBuiltinEvents(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN, false);
-  events = events.filter(e => e[1] == "navigation" && e[2] == "search");
+  Assert.ok("default" in events, "We should have recorded events in the parent process.");
+  events = events.default.filter(e => e[1] == "navigation" && e[2] == "search");
   checkEvents(events, [["navigation", "search", "searchbar", "enter", {engine: "other-MozSearch"}]]);
 
   yield BrowserTestUtils.removeTab(tab);
 });
 
 add_task(function* test_oneOff() {
   // Let's reset the counts.
   Services.telemetry.clearScalars();
@@ -136,17 +137,18 @@ add_task(function* test_oneOff() {
   Assert.equal(Object.keys(scalars[SCALAR_SEARCHBAR]).length, 1,
                "This search must only increment one entry in the scalar.");
 
   // Make sure SEARCH_COUNTS contains identical values.
   checkKeyedHistogram(search_hist, "other-MozSearch2.searchbar", 1);
 
   // Also check events.
   let events = Services.telemetry.snapshotBuiltinEvents(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN, false);
-  events = events.filter(e => e[1] == "navigation" && e[2] == "search");
+  Assert.ok("default" in events, "We should have recorded events in the parent process.");
+  events = events.default.filter(e => e[1] == "navigation" && e[2] == "search");
   checkEvents(events, [["navigation", "search", "searchbar", "oneoff", {engine: "other-MozSearch2"}]]);
 
   yield BrowserTestUtils.removeTab(tab);
 });
 
 add_task(function* test_suggestion() {
   // Let's reset the counts.
   Services.telemetry.clearScalars();
@@ -182,15 +184,16 @@ add_task(function* test_suggestion() {
                "This search must only increment one entry in the scalar.");
 
   // Make sure SEARCH_COUNTS contains identical values.
   let searchEngineId = "other-" + suggestionEngine.name;
   checkKeyedHistogram(search_hist, searchEngineId + ".searchbar", 1);
 
   // Also check events.
   let events = Services.telemetry.snapshotBuiltinEvents(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN, false);
-  events = events.filter(e => e[1] == "navigation" && e[2] == "search");
+  Assert.ok("default" in events, "We should have recorded events in the parent process.");
+  events = events.default.filter(e => e[1] == "navigation" && e[2] == "search");
   checkEvents(events, [["navigation", "search", "searchbar", "suggestion", {engine: searchEngineId}]]);
 
   Services.search.currentEngine = previousEngine;
   Services.search.removeEngine(suggestionEngine);
   yield BrowserTestUtils.removeTab(tab);
 });
--- a/browser/modules/test/browser_UsageTelemetry_urlbar.js
+++ b/browser/modules/test/browser_UsageTelemetry_urlbar.js
@@ -121,17 +121,18 @@ add_task(function* test_simpleQuery() {
   Assert.equal(Object.keys(scalars[SCALAR_URLBAR]).length, 1,
                "This search must only increment one entry in the scalar.");
 
   // Make sure SEARCH_COUNTS contains identical values.
   checkKeyedHistogram(search_hist, "other-MozSearch.urlbar", 1);
 
   // Also check events.
   let events = Services.telemetry.snapshotBuiltinEvents(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN, false);
-  events = events.filter(e => e[1] == "navigation" && e[2] == "search");
+  Assert.ok("default" in events, "We should have recorded events in the parent process.");
+  events = events.default.filter(e => e[1] == "navigation" && e[2] == "search");
   checkEvents(events, [["navigation", "search", "urlbar", "enter", {engine: "other-MozSearch"}]]);
 
   // Check the histograms as well.
   let resultIndexes = resultIndexHist.snapshot();
   checkHistogramResults(resultIndexes, 0, "FX_URLBAR_SELECTED_RESULT_INDEX");
 
   let resultTypes = resultTypeHist.snapshot();
   checkHistogramResults(resultTypes,
@@ -166,17 +167,18 @@ add_task(function* test_searchAlias() {
   Assert.equal(Object.keys(scalars[SCALAR_URLBAR]).length, 1,
                "This search must only increment one entry in the scalar.");
 
   // Make sure SEARCH_COUNTS contains identical values.
   checkKeyedHistogram(search_hist, "other-MozSearch.urlbar", 1);
 
   // Also check events.
   let events = Services.telemetry.snapshotBuiltinEvents(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN, false);
-  events = events.filter(e => e[1] == "navigation" && e[2] == "search");
+  Assert.ok("default" in events, "We should have recorded events in the parent process.");
+  events = events.default.filter(e => e[1] == "navigation" && e[2] == "search");
   checkEvents(events, [["navigation", "search", "urlbar", "alias", {engine: "other-MozSearch"}]]);
 
   // Check the histograms as well.
   let resultIndexes = resultIndexHist.snapshot();
   checkHistogramResults(resultIndexes, 0, "FX_URLBAR_SELECTED_RESULT_INDEX");
 
   let resultTypes = resultTypeHist.snapshot();
   checkHistogramResults(resultTypes,
@@ -214,17 +216,18 @@ add_task(function* test_oneOff() {
   Assert.equal(Object.keys(scalars[SCALAR_URLBAR]).length, 1,
                "This search must only increment one entry in the scalar.");
 
   // Make sure SEARCH_COUNTS contains identical values.
   checkKeyedHistogram(search_hist, "other-MozSearch.urlbar", 1);
 
   // Also check events.
   let events = Services.telemetry.snapshotBuiltinEvents(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN, false);
-  events = events.filter(e => e[1] == "navigation" && e[2] == "search");
+  Assert.ok("default" in events, "We should have recorded events in the parent process.");
+  events = events.default.filter(e => e[1] == "navigation" && e[2] == "search");
   checkEvents(events, [["navigation", "search", "urlbar", "oneoff", {engine: "other-MozSearch"}]]);
 
   // Check the histograms as well.
   let resultIndexes = resultIndexHist.snapshot();
   checkHistogramResults(resultIndexes, 0, "FX_URLBAR_SELECTED_RESULT_INDEX");
 
   let resultTypes = resultTypeHist.snapshot();
   checkHistogramResults(resultTypes,
@@ -274,17 +277,18 @@ add_task(function* test_suggestion() {
                "This search must only increment one entry in the scalar.");
 
   // Make sure SEARCH_COUNTS contains identical values.
   let searchEngineId = "other-" + suggestionEngine.name;
   checkKeyedHistogram(search_hist, searchEngineId + ".urlbar", 1);
 
   // Also check events.
   let events = Services.telemetry.snapshotBuiltinEvents(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN, false);
-  events = events.filter(e => e[1] == "navigation" && e[2] == "search");
+  Assert.ok("default" in events, "We should have recorded events in the parent process.");
+  events = events.default.filter(e => e[1] == "navigation" && e[2] == "search");
   checkEvents(events, [["navigation", "search", "urlbar", "suggestion", {engine: searchEngineId}]]);
 
   // Check the histograms as well.
   let resultIndexes = resultIndexHist.snapshot();
   checkHistogramResults(resultIndexes, 3, "FX_URLBAR_SELECTED_RESULT_INDEX");
 
   let resultTypes = resultTypeHist.snapshot();
   checkHistogramResults(resultTypes,
--- a/dom/ipc/ContentParent.cpp
+++ b/dom/ipc/ContentParent.cpp
@@ -5073,16 +5073,23 @@ ContentParent::RecvUpdateChildScalars(
 mozilla::ipc::IPCResult
 ContentParent::RecvUpdateChildKeyedScalars(
                 InfallibleTArray<KeyedScalarAction>&& aScalarActions)
 {
   TelemetryIPC::UpdateChildKeyedScalars(GeckoProcessType_Content, aScalarActions);
   return IPC_OK();
 }
 
+mozilla::ipc::IPCResult
+ContentParent::RecvRecordChildEvents(nsTArray<mozilla::Telemetry::ChildEventData>&& aEvents)
+{
+  TelemetryIPC::RecordChildEvents(GeckoProcessType_Content, aEvents);
+  return IPC_OK();
+}
+
 PURLClassifierParent*
 ContentParent::AllocPURLClassifierParent(const Principal& aPrincipal,
                                          const bool& aUseTrackingProtection,
                                          bool* aSuccess)
 {
   MOZ_ASSERT(NS_IsMainThread());
 
   *aSuccess = true;
--- a/dom/ipc/ContentParent.h
+++ b/dom/ipc/ContentParent.h
@@ -1117,16 +1117,18 @@ private:
   virtual mozilla::ipc::IPCResult RecvAccumulateChildHistograms(
     InfallibleTArray<Accumulation>&& aAccumulations) override;
   virtual mozilla::ipc::IPCResult RecvAccumulateChildKeyedHistograms(
     InfallibleTArray<KeyedAccumulation>&& aAccumulations) override;
   virtual mozilla::ipc::IPCResult RecvUpdateChildScalars(
     InfallibleTArray<ScalarAction>&& aScalarActions) override;
   virtual mozilla::ipc::IPCResult RecvUpdateChildKeyedScalars(
     InfallibleTArray<KeyedScalarAction>&& aScalarActions) override;
+  virtual mozilla::ipc::IPCResult RecvRecordChildEvents(
+    nsTArray<ChildEventData>&& events) override;
 public:
   void SendGetFilesResponseAndForget(const nsID& aID,
                                      const GetFilesResponseResult& aResult);
 
   bool SendRequestMemoryReport(const uint32_t& aGeneration,
                                const bool& aAnonymize,
                                const bool& aMinimizeMemoryUsage,
                                const MaybeFileDesc& aDMDFile) override;
--- a/dom/ipc/PContent.ipdl
+++ b/dom/ipc/PContent.ipdl
@@ -85,16 +85,17 @@ using class mozilla::dom::ipc::Structure
 using mozilla::DataStorageType from "ipc/DataStorageIPCUtils.h";
 using mozilla::OriginAttributes from "mozilla/ipc/BackgroundUtils.h";
 using struct mozilla::layers::TextureFactoryIdentifier from "mozilla/layers/CompositorTypes.h";
 using struct mozilla::dom::FlyWebPublishOptions from "mozilla/dom/FlyWebPublishOptionsIPCSerializer.h";
 using mozilla::Telemetry::Accumulation from "mozilla/TelemetryComms.h";
 using mozilla::Telemetry::KeyedAccumulation from "mozilla/TelemetryComms.h";
 using mozilla::Telemetry::ScalarAction from "mozilla/TelemetryComms.h";
 using mozilla::Telemetry::KeyedScalarAction from "mozilla/TelemetryComms.h";
+using mozilla::Telemetry::ChildEventData from "mozilla/TelemetryComms.h";
 
 union ChromeRegistryItem
 {
     ChromePackage;
     OverrideMapping;
     SubstitutionMapping;
 };
 
@@ -1193,16 +1194,17 @@ parent:
 
     /**
      * Messages for communicating child Telemetry to the parent process
      */
     async AccumulateChildHistograms(Accumulation[] accumulations);
     async AccumulateChildKeyedHistograms(KeyedAccumulation[] accumulations);
     async UpdateChildScalars(ScalarAction[] updates);
     async UpdateChildKeyedScalars(KeyedScalarAction[] updates);
+    async RecordChildEvents(ChildEventData[] events);
 
     sync GetA11yContentId() returns (uint32_t aContentId);
 
     async AddMemoryReport(MemoryReport aReport);
     async FinishMemoryReport(uint32_t aGeneration);
 
 both:
      async AsyncMessage(nsString aMessage, CpowEntry[] aCpows,
--- a/gfx/ipc/GPUChild.cpp
+++ b/gfx/ipc/GPUChild.cpp
@@ -168,16 +168,23 @@ GPUChild::RecvUpdateChildScalars(Infalli
 mozilla::ipc::IPCResult
 GPUChild::RecvUpdateChildKeyedScalars(InfallibleTArray<KeyedScalarAction>&& aScalarActions)
 {
   TelemetryIPC::UpdateChildKeyedScalars(GeckoProcessType_GPU, aScalarActions);
   return IPC_OK();
 }
 
 mozilla::ipc::IPCResult
+GPUChild::RecvRecordChildEvents(nsTArray<mozilla::Telemetry::ChildEventData>&& aEvents)
+{
+  TelemetryIPC::RecordChildEvents(GeckoProcessType_GPU, aEvents);
+  return IPC_OK();
+}
+
+mozilla::ipc::IPCResult
 GPUChild::RecvNotifyDeviceReset()
 {
   mHost->mListener->OnProcessDeviceReset(mHost);
   return IPC_OK();
 }
 
 bool
 GPUChild::SendRequestMemoryReport(const uint32_t& aGeneration,
--- a/gfx/ipc/GPUChild.h
+++ b/gfx/ipc/GPUChild.h
@@ -38,20 +38,23 @@ public:
 
   // gfxVarReceiver overrides.
   void OnVarChanged(const GfxVarUpdate& aVar) override;
 
   // PGPUChild overrides.
   mozilla::ipc::IPCResult RecvInitComplete(const GPUDeviceData& aData) override;
   mozilla::ipc::IPCResult RecvReportCheckerboard(const uint32_t& aSeverity, const nsCString& aLog) override;
   mozilla::ipc::IPCResult RecvInitCrashReporter(Shmem&& shmem, const NativeThreadId& aThreadId) override;
+
   mozilla::ipc::IPCResult RecvAccumulateChildHistograms(InfallibleTArray<Accumulation>&& aAccumulations) override;
   mozilla::ipc::IPCResult RecvAccumulateChildKeyedHistograms(InfallibleTArray<KeyedAccumulation>&& aAccumulations) override;
   mozilla::ipc::IPCResult RecvUpdateChildScalars(InfallibleTArray<ScalarAction>&& aScalarActions) override;
   mozilla::ipc::IPCResult RecvUpdateChildKeyedScalars(InfallibleTArray<KeyedScalarAction>&& aScalarActions) override;
+  mozilla::ipc::IPCResult RecvRecordChildEvents(nsTArray<ChildEventData>&& events) override;
+
   void ActorDestroy(ActorDestroyReason aWhy) override;
   mozilla::ipc::IPCResult RecvGraphicsError(const nsCString& aError) override;
   mozilla::ipc::IPCResult RecvNotifyUiObservers(const nsCString& aTopic) override;
   mozilla::ipc::IPCResult RecvNotifyDeviceReset() override;
   mozilla::ipc::IPCResult RecvAddMemoryReport(const MemoryReport& aReport) override;
   mozilla::ipc::IPCResult RecvFinishMemoryReport(const uint32_t& aGeneration) override;
 
   bool SendRequestMemoryReport(const uint32_t& aGeneration,
--- a/gfx/ipc/PGPU.ipdl
+++ b/gfx/ipc/PGPU.ipdl
@@ -17,16 +17,17 @@ using mozilla::TimeDuration from "mozill
 using mozilla::CSSToLayoutDeviceScale from "Units.h";
 using mozilla::dom::NativeThreadId from "mozilla/dom/TabMessageUtils.h";
 using mozilla::gfx::IntSize from "mozilla/gfx/2D.h";
 using mozilla::layers::CompositorOptions from "mozilla/layers/CompositorOptions.h";
 using mozilla::Telemetry::Accumulation from "mozilla/TelemetryComms.h";
 using mozilla::Telemetry::KeyedAccumulation from "mozilla/TelemetryComms.h";
 using mozilla::Telemetry::ScalarAction from "mozilla/TelemetryComms.h";
 using mozilla::Telemetry::KeyedScalarAction from "mozilla/TelemetryComms.h";
+using mozilla::Telemetry::ChildEventData from "mozilla/TelemetryComms.h";
 
 namespace mozilla {
 namespace gfx {
 
 union GfxPrefValue {
   bool;
   int32_t;
   uint32_t;
@@ -109,16 +110,17 @@ child:
   // observer service.
   async NotifyUiObservers(nsCString aTopic);
 
   // Messages for reporting telemetry to the UI process.
   async AccumulateChildHistograms(Accumulation[] accumulations);
   async AccumulateChildKeyedHistograms(KeyedAccumulation[] accumulations);
   async UpdateChildScalars(ScalarAction[] actions);
   async UpdateChildKeyedScalars(KeyedScalarAction[] actions);
+  async RecordChildEvents(ChildEventData[] events);
 
   async NotifyDeviceReset();
 
   async AddMemoryReport(MemoryReport aReport);
   async FinishMemoryReport(uint32_t aGeneration);
 };
 
 } // namespace gfx
--- a/toolkit/components/telemetry/Telemetry.h
+++ b/toolkit/components/telemetry/Telemetry.h
@@ -33,16 +33,17 @@ namespace HangMonitor {
   class HangAnnotations;
 } // namespace HangMonitor
 namespace Telemetry {
 
 struct Accumulation;
 struct KeyedAccumulation;
 struct ScalarAction;
 struct KeyedScalarAction;
+struct ChildEventData;
 
 enum TimerResolution {
   Millisecond,
   Microsecond
 };
 
 /**
  * Create and destroy the underlying base::StatisticsRecorder singleton.
--- a/toolkit/components/telemetry/TelemetryEvent.cpp
+++ b/toolkit/components/telemetry/TelemetryEvent.cpp
@@ -9,38 +9,43 @@
 #include "nsHashKeys.h"
 #include "nsDataHashtable.h"
 #include "nsClassHashtable.h"
 #include "nsTArray.h"
 #include "mozilla/StaticMutex.h"
 #include "mozilla/Unused.h"
 #include "mozilla/Maybe.h"
 #include "mozilla/StaticPtr.h"
+#include "mozilla/Pair.h"
 #include "jsapi.h"
 #include "nsJSUtils.h"
 #include "nsXULAppAPI.h"
 #include "nsUTF8Utils.h"
 
 #include "TelemetryCommon.h"
 #include "TelemetryEvent.h"
 #include "TelemetryEventData.h"
+#include "ipc/TelemetryIPCAccumulator.h"
 
 using mozilla::StaticMutex;
 using mozilla::StaticMutexAutoLock;
 using mozilla::ArrayLength;
 using mozilla::Maybe;
 using mozilla::Nothing;
-using mozilla::Pair;
 using mozilla::StaticAutoPtr;
 using mozilla::Telemetry::Common::AutoHashtable;
 using mozilla::Telemetry::Common::IsExpiredVersion;
 using mozilla::Telemetry::Common::CanRecordDataset;
 using mozilla::Telemetry::Common::IsInDataset;
 using mozilla::Telemetry::Common::MsSinceProcessStart;
 using mozilla::Telemetry::Common::LogToBrowserConsole;
+using mozilla::Telemetry::EventExtraEntry;
+using mozilla::Telemetry::ChildEventData;
+
+namespace TelemetryIPCAccumulator = mozilla::TelemetryIPCAccumulator;
 
 ////////////////////////////////////////////////////////////////////////
 ////////////////////////////////////////////////////////////////////////
 //
 // Naming: there are two kinds of functions in this file:
 //
 // * Functions taking a StaticMutexAutoLock: these can only be reached via
 //   an interface function (TelemetryEvent::*). They expect the interface
@@ -98,22 +103,17 @@ typedef nsClassHashtable<nsCStringHashKe
 
 enum class RecordEventResult {
   Ok,
   UnknownEvent,
   InvalidExtraKey,
   StorageLimitReached,
 };
 
-struct ExtraEntry {
-  const nsCString key;
-  const nsCString value;
-};
-
-typedef nsTArray<ExtraEntry> ExtraArray;
+typedef nsTArray<EventExtraEntry> ExtraArray;
 
 class EventRecord {
 public:
   EventRecord(double timestamp, uint32_t eventId, const Maybe<nsCString>& value,
               const ExtraArray& extra)
     : mTimestamp(timestamp)
     , mEventId(eventId)
     , mValue(value)
@@ -253,25 +253,27 @@ bool gCanRecordExtended;
 StringUintMap gEventNameIDMap(kEventCount);
 
 // The CategoryName -> CategoryID cache map.
 StringUintMap gCategoryNameIDMap;
 
 // This tracks the IDs of the categories for which recording is enabled.
 nsTHashtable<nsUint32HashKey> gEnabledCategories;
 
-// The main event storage. Events are inserted here in recording order.
-StaticAutoPtr<nsTArray<EventRecord>> gEventRecords;
+// The main event storage. Events are inserted here, keyed by process id and
+// in recording order.
+typedef nsTArray<EventRecord> EventRecordArray;
+nsClassHashtable<nsUint32HashKey, EventRecordArray> gEventRecords;
 
 } // namespace
 
 ////////////////////////////////////////////////////////////////////////
 ////////////////////////////////////////////////////////////////////////
 //
-// PRIVATE: thread-unsafe helpers for event recording.
+// PRIVATE: thread-safe helpers for event recording.
 
 namespace {
 
 bool
 CanRecordEvent(const StaticMutexAutoLock& lock, const CommonEventInfo& info)
 {
   if (!gCanRecordBase) {
     return false;
@@ -279,24 +281,37 @@ CanRecordEvent(const StaticMutexAutoLock
 
   if (!CanRecordDataset(info.dataset, gCanRecordBase, gCanRecordExtended)) {
     return false;
   }
 
   return gEnabledCategories.GetEntry(info.category_offset);
 }
 
+EventRecordArray*
+GetEventRecordsForProcess(const StaticMutexAutoLock& lock, GeckoProcessType processType)
+{
+  EventRecordArray* eventRecords = nullptr;
+  if (!gEventRecords.Get(processType, &eventRecords)) {
+    eventRecords = new EventRecordArray();
+    gEventRecords.Put(processType, eventRecords);
+  }
+  return eventRecords;
+}
+
 RecordEventResult
-RecordEvent(const StaticMutexAutoLock& lock, double timestamp,
-            const nsACString& category, const nsACString& method,
-            const nsACString& object, const Maybe<nsCString>& value,
-            const ExtraArray& extra)
+RecordEvent(const StaticMutexAutoLock& lock, GeckoProcessType processType,
+            double timestamp, const nsACString& category,
+            const nsACString& method, const nsACString& object,
+            const Maybe<nsCString>& value, const ExtraArray& extra)
 {
+  EventRecordArray* eventRecords = GetEventRecordsForProcess(lock, processType);
+
   // Apply hard limit on event count in storage.
-  if (gEventRecords->Length() >= kMaxEventRecords) {
+  if (eventRecords->Length() >= kMaxEventRecords) {
     return RecordEventResult::StorageLimitReached;
   }
 
   // Look up the event id.
   const nsCString& name = UniqueEventName(category, method, object);
   uint32_t eventId;
   if (!gEventNameIDMap.Get(name, &eventId)) {
     return RecordEventResult::UnknownEvent;
@@ -324,25 +339,126 @@ RecordEvent(const StaticMutexAutoLock& l
 
   for (uint32_t i = 0; i < extra.Length(); ++i) {
     if (!validExtraKeys.GetEntry(extra[i].key)) {
       return RecordEventResult::InvalidExtraKey;
     }
   }
 
   // Add event record.
-  gEventRecords->AppendElement(EventRecord(timestamp, eventId, value, extra));
+  eventRecords->AppendElement(EventRecord(timestamp, eventId, value, extra));
   return RecordEventResult::Ok;
 }
 
 } // anonymous namespace
 
 ////////////////////////////////////////////////////////////////////////
 ////////////////////////////////////////////////////////////////////////
 //
+// PRIVATE: thread-unsafe helpers for event handling.
+
+namespace {
+
+nsresult
+SerializeEventsArray(const EventRecordArray& events,
+                    JSContext* cx,
+                    JS::MutableHandleObject result)
+{
+  // We serialize the events to a JS array.
+  JS::RootedObject eventsArray(cx, JS_NewArrayObject(cx, events.Length()));
+  if (!eventsArray) {
+    return NS_ERROR_FAILURE;
+  }
+
+  for (uint32_t i = 0; i < events.Length(); ++i) {
+    const EventRecord& record = events[i];
+    const EventInfo& info = gEventInfo[record.EventId()];
+
+    // Each entry is an array of one of the forms:
+    // [timestamp, category, method, object, value]
+    // [timestamp, category, method, object, null, extra]
+    // [timestamp, category, method, object, value, extra]
+    JS::AutoValueVector items(cx);
+
+    // Add timestamp.
+    JS::Rooted<JS::Value> val(cx);
+    if (!items.append(JS::NumberValue(floor(record.Timestamp())))) {
+      return NS_ERROR_FAILURE;
+    }
+
+    // Add category, method, object.
+    const char* strings[] = {
+      info.common_info.category(),
+      info.method(),
+      info.object(),
+    };
+    for (const char* s : strings) {
+      const NS_ConvertUTF8toUTF16 wide(s);
+      if (!items.append(JS::StringValue(JS_NewUCStringCopyN(cx, wide.Data(), wide.Length())))) {
+        return NS_ERROR_FAILURE;
+      }
+    }
+
+    // Add the optional string value only when needed.
+    // When the value field is empty and extra is not set, we can save a little space that way.
+    // We still need to submit a null value if extra is set, to match the form:
+    // [ts, category, method, object, null, extra]
+    if (record.Value()) {
+      const NS_ConvertUTF8toUTF16 wide(record.Value().value());
+      if (!items.append(JS::StringValue(JS_NewUCStringCopyN(cx, wide.Data(), wide.Length())))) {
+        return NS_ERROR_FAILURE;
+      }
+    } else if (!record.Extra().IsEmpty()) {
+      if (!items.append(JS::NullValue())) {
+        return NS_ERROR_FAILURE;
+      }
+    }
+
+    // Add the optional extra dictionary.
+    // To save a little space, only add it when it is not empty.
+    if (!record.Extra().IsEmpty()) {
+      JS::RootedObject obj(cx, JS_NewPlainObject(cx));
+      if (!obj) {
+        return NS_ERROR_FAILURE;
+      }
+
+      // Add extra key & value entries.
+      const ExtraArray& extra = record.Extra();
+      for (uint32_t i = 0; i < extra.Length(); ++i) {
+        const NS_ConvertUTF8toUTF16 wide(extra[i].value);
+        JS::Rooted<JS::Value> value(cx);
+        value.setString(JS_NewUCStringCopyN(cx, wide.Data(), wide.Length()));
+
+        if (!JS_DefineProperty(cx, obj, extra[i].key.get(), value, JSPROP_ENUMERATE)) {
+          return NS_ERROR_FAILURE;
+        }
+      }
+      val.setObject(*obj);
+
+      if (!items.append(val)) {
+        return NS_ERROR_FAILURE;
+      }
+    }
+
+    // Add the record to the events array.
+    JS::RootedObject itemsArray(cx, JS_NewArrayObject(cx, items));
+    if (!JS_DefineElement(cx, eventsArray, i, itemsArray, JSPROP_ENUMERATE)) {
+      return NS_ERROR_FAILURE;
+    }
+  }
+
+  result.set(eventsArray);
+  return NS_OK;
+}
+
+} // anonymous namespace
+
+////////////////////////////////////////////////////////////////////////
+////////////////////////////////////////////////////////////////////////
+//
 // EXTERNALLY VISIBLE FUNCTIONS in namespace TelemetryEvents::
 
 // This is a StaticMutex rather than a plain Mutex (1) so that
 // it gets initialised in a thread-safe manner the first time
 // it is used, and (2) because it is never de-initialised, and
 // a normal Mutex would show up as a leak in BloatView.  StaticMutex
 // also has the "OffTheBooks" property, so it won't show as a leak
 // in BloatView.
@@ -357,18 +473,16 @@ TelemetryEvent::InitializeGlobalState(bo
 {
   StaticMutexAutoLock locker(gTelemetryEventsMutex);
   MOZ_ASSERT(!gInitDone, "TelemetryEvent::InitializeGlobalState "
              "may only be called once");
 
   gCanRecordBase = aCanRecordBase;
   gCanRecordExtended = aCanRecordExtended;
 
-  gEventRecords = new nsTArray<EventRecord>();
-
   // Populate the static event name->id cache. Note that the event names are
   // statically allocated and come from the automatically generated TelemetryEventData.h.
   const uint32_t eventCount = static_cast<uint32_t>(mozilla::Telemetry::EventID::EventCount);
   for (uint32_t i = 0; i < eventCount; ++i) {
     const EventInfo& info = gEventInfo[i];
     uint32_t eventId = i;
 
     // If this event is expired, mark it with a special event id.
@@ -399,18 +513,17 @@ TelemetryEvent::DeInitializeGlobalState(
   MOZ_ASSERT(gInitDone);
 
   gCanRecordBase = false;
   gCanRecordExtended = false;
 
   gEventNameIDMap.Clear();
   gCategoryNameIDMap.Clear();
   gEnabledCategories.Clear();
-  gEventRecords->Clear();
-  gEventRecords = nullptr;
+  gEventRecords.Clear();
 
   gInitDone = false;
 }
 
 void
 TelemetryEvent::SetCanRecordBase(bool b)
 {
   StaticMutexAutoLock locker(gTelemetryEventsMutex);
@@ -419,26 +532,34 @@ TelemetryEvent::SetCanRecordBase(bool b)
 
 void
 TelemetryEvent::SetCanRecordExtended(bool b) {
   StaticMutexAutoLock locker(gTelemetryEventsMutex);
   gCanRecordExtended = b;
 }
 
 nsresult
+TelemetryEvent::RecordChildEvents(GeckoProcessType aProcessType,
+                                  const nsTArray<mozilla::Telemetry::ChildEventData>& aEvents)
+{
+  MOZ_ASSERT(XRE_IsParentProcess());
+  StaticMutexAutoLock locker(gTelemetryEventsMutex);
+  for (uint32_t i = 0; i < aEvents.Length(); ++i) {
+    const mozilla::Telemetry::ChildEventData e = aEvents[i];
+    ::RecordEvent(locker, aProcessType, e.timestamp, e.category, e.method, e.object, e.value, e.extra);
+  }
+  return NS_OK;
+}
+
+nsresult
 TelemetryEvent::RecordEvent(const nsACString& aCategory, const nsACString& aMethod,
                             const nsACString& aObject, JS::HandleValue aValue,
                             JS::HandleValue aExtra, JSContext* cx,
                             uint8_t optional_argc)
 {
-  // Currently only recording in the parent process is supported.
-  if (!XRE_IsParentProcess()) {
-    return NS_OK;
-  }
-
   // Get the current time.
   double timestamp = -1;
   nsresult rv = MsSinceProcessStart(&timestamp);
   if (NS_FAILED(rv)) {
     LogToBrowserConsole(nsIScriptError::warningFlag,
                         NS_LITERAL_STRING("Failed to get time since process start."));
     return NS_OK;
   }
@@ -511,32 +632,37 @@ TelemetryEvent::RecordEvent(const nsACSt
 
       nsCString str = NS_ConvertUTF16toUTF8(jsStr);
       if (str.Length() > kMaxExtraValueByteLength) {
         LogToBrowserConsole(nsIScriptError::warningFlag,
                             NS_LITERAL_STRING("Extra value exceeds maximum string length, truncating."));
         TruncateToByteLength(str, kMaxExtraValueByteLength);
       }
 
-      extra.AppendElement(ExtraEntry{NS_ConvertUTF16toUTF8(key), str});
+      extra.AppendElement(EventExtraEntry{NS_ConvertUTF16toUTF8(key), str});
     }
   }
 
+  if (!XRE_IsParentProcess()) {
+    TelemetryIPCAccumulator::RecordChildEvent(timestamp, aCategory, aMethod, aObject, value, extra);
+    return NS_OK;
+  }
+
   // Lock for accessing internal data.
   // While the lock is being held, no complex calls like JS calls can be made,
   // as all of these could record Telemetry, which would result in deadlock.
   RecordEventResult res;
   {
     StaticMutexAutoLock lock(gTelemetryEventsMutex);
 
     if (!gInitDone) {
       return NS_ERROR_FAILURE;
     }
 
-    res = ::RecordEvent(lock, timestamp, aCategory, aMethod, aObject, value, extra);
+    res = ::RecordEvent(lock, GeckoProcessType_Default, timestamp, aCategory, aMethod, aObject, value, extra);
   }
 
   // Trigger warnings or errors where needed.
   switch (res) {
     case RecordEventResult::UnknownEvent: {
       JS_ReportErrorASCII(cx, R"(Unknown event: ["%s", "%s", "%s"])",
                           PromiseFlatCString(aCategory).get(),
                           PromiseFlatCString(aMethod).get(),
@@ -555,138 +681,96 @@ TelemetryEvent::RecordEvent(const nsACSt
       return NS_OK;
   }
 }
 
 nsresult
 TelemetryEvent::CreateSnapshots(uint32_t aDataset, bool aClear, JSContext* cx,
                                 uint8_t optional_argc, JS::MutableHandleValue aResult)
 {
-  // Extract the events from storage.
-  nsTArray<EventRecord> events;
+  if (!XRE_IsParentProcess()) {
+    return NS_ERROR_FAILURE;
+  }
+
+  // Creating a JS snapshot of the events is a two-step process:
+  // (1) Lock the storage and copy the events into function-local storage.
+  // (2) Serialize the events into JS.
+  // We can't hold a lock for (2) because we will run into deadlocks otherwise
+  // from JS recording Telemetry.
+
+  // (1) Extract the events from storage with a lock held.
+  nsTArray<mozilla::Pair<const char*, EventRecordArray>> processEvents;
   {
     StaticMutexAutoLock locker(gTelemetryEventsMutex);
 
     if (!gInitDone) {
       return NS_ERROR_FAILURE;
     }
 
-    uint32_t len = gEventRecords->Length();
-    for (uint32_t i = 0; i < len; ++i) {
-      const EventRecord& record = (*gEventRecords)[i];
-      const EventInfo& info = gEventInfo[record.EventId()];
+    for (auto iter = gEventRecords.Iter(); !iter.Done(); iter.Next()) {
+      const EventRecordArray* eventStorage = static_cast<EventRecordArray*>(iter.Data());
+      EventRecordArray events;
+
+      const uint32_t len = eventStorage->Length();
+      for (uint32_t i = 0; i < len; ++i) {
+        const EventRecord& record = (*eventStorage)[i];
+        const EventInfo& info = gEventInfo[record.EventId()];
 
-      if (IsInDataset(info.common_info.dataset, aDataset)) {
-        events.AppendElement(record);
+        if (IsInDataset(info.common_info.dataset, aDataset)) {
+          events.AppendElement(record);
+        }
+      }
+
+      if (events.Length()) {
+        const char* processName = XRE_ChildProcessTypeToString(GeckoProcessType(iter.Key()));
+        processEvents.AppendElement(mozilla::MakePair(processName, events));
       }
     }
 
     if (aClear) {
-      gEventRecords->Clear();
+      gEventRecords.Clear();
     }
   }
 
-  // We serialize the events to a JS array.
-  JS::RootedObject eventsArray(cx, JS_NewArrayObject(cx, events.Length()));
-  if (!eventsArray) {
+  // (2) Serialize the events to a JS object.
+  JS::RootedObject rootObj(cx, JS_NewPlainObject(cx));
+  if (!rootObj) {
     return NS_ERROR_FAILURE;
   }
 
-  for (uint32_t i = 0; i < events.Length(); ++i) {
-    const EventRecord& record = events[i];
-    const EventInfo& info = gEventInfo[record.EventId()];
-
-    // Each entry is an array of one of the forms:
-    // [timestamp, category, method, object, value]
-    // [timestamp, category, method, object, null, extra]
-    // [timestamp, category, method, object, value, extra]
-    JS::AutoValueVector items(cx);
-
-    // Add timestamp.
-    JS::Rooted<JS::Value> val(cx);
-    if (!items.append(JS::NumberValue(floor(record.Timestamp())))) {
+  const uint32_t processLength = processEvents.Length();
+  for (uint32_t i = 0; i < processLength; ++i)
+  {
+    JS::RootedObject eventsArray(cx);
+    if (NS_FAILED(SerializeEventsArray(processEvents[i].second(), cx, &eventsArray))) {
       return NS_ERROR_FAILURE;
     }
 
-    // Add category, method, object.
-    const char* strings[] = {
-      info.common_info.category(),
-      info.method(),
-      info.object(),
-    };
-    for (const char* s : strings) {
-      const NS_ConvertUTF8toUTF16 wide(s);
-      if (!items.append(JS::StringValue(JS_NewUCStringCopyN(cx, wide.Data(), wide.Length())))) {
-        return NS_ERROR_FAILURE;
-      }
-    }
-
-    // Add the optional string value only when needed.
-    // When extra is empty and this has no value, we can save a little space.
-    if (record.Value()) {
-      const NS_ConvertUTF8toUTF16 wide(record.Value().value());
-      if (!items.append(JS::StringValue(JS_NewUCStringCopyN(cx, wide.Data(), wide.Length())))) {
-        return NS_ERROR_FAILURE;
-      }
-    } else if (!record.Extra().IsEmpty()) {
-      if (!items.append(JS::NullValue())) {
-        return NS_ERROR_FAILURE;
-      }
-    }
-
-    // Add the optional extra dictionary.
-    // To save a little space, only add it when it is not empty.
-    if (!record.Extra().IsEmpty()) {
-      JS::RootedObject obj(cx, JS_NewPlainObject(cx));
-      if (!obj) {
-        return NS_ERROR_FAILURE;
-      }
-
-      // Add extra key & value entries.
-      const ExtraArray& extra = record.Extra();
-      for (uint32_t i = 0; i < extra.Length(); ++i) {
-        const NS_ConvertUTF8toUTF16 wide(extra[i].value);
-        JS::Rooted<JS::Value> value(cx);
-        value.setString(JS_NewUCStringCopyN(cx, wide.Data(), wide.Length()));
-
-        if (!JS_DefineProperty(cx, obj, extra[i].key.get(), value, JSPROP_ENUMERATE)) {
-          return NS_ERROR_FAILURE;
-        }
-      }
-      val.setObject(*obj);
-
-      if (!items.append(val)) {
-        return NS_ERROR_FAILURE;
-      }
-    }
-
-    // Add the record to the events array.
-    JS::RootedObject itemsArray(cx, JS_NewArrayObject(cx, items));
-    if (!JS_DefineElement(cx, eventsArray, i, itemsArray, JSPROP_ENUMERATE)) {
+    if (!JS_DefineProperty(cx, rootObj, processEvents[i].first(), eventsArray, JSPROP_ENUMERATE)) {
       return NS_ERROR_FAILURE;
     }
   }
 
-  aResult.setObject(*eventsArray);
+  aResult.setObject(*rootObj);
   return NS_OK;
 }
 
 /**
  * Resets all the stored events. This is intended to be only used in tests.
  */
 void
 TelemetryEvent::ClearEvents()
 {
   StaticMutexAutoLock lock(gTelemetryEventsMutex);
 
   if (!gInitDone) {
     return;
   }
 
-  gEventRecords->Clear();
+  gEventRecords.Clear();
 }
 
 void
 TelemetryEvent::SetEventRecordingEnabled(const nsACString& category, bool enabled)
 {
   StaticMutexAutoLock locker(gTelemetryEventsMutex);
 
   uint32_t categoryId;
@@ -704,19 +788,26 @@ TelemetryEvent::SetEventRecordingEnabled
 }
 
 size_t
 TelemetryEvent::SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf)
 {
   StaticMutexAutoLock locker(gTelemetryEventsMutex);
   size_t n = 0;
 
-  n += gEventRecords->ShallowSizeOfIncludingThis(aMallocSizeOf);
-  for (uint32_t i = 0; i < gEventRecords->Length(); ++i) {
-    n += (*gEventRecords)[i].SizeOfExcludingThis(aMallocSizeOf);
+
+  n += gEventRecords.ShallowSizeOfExcludingThis(aMallocSizeOf);
+  for (auto iter = gEventRecords.Iter(); !iter.Done(); iter.Next()) {
+    EventRecordArray* eventRecords = static_cast<EventRecordArray*>(iter.Data());
+    n += eventRecords->ShallowSizeOfIncludingThis(aMallocSizeOf);
+
+    const uint32_t len = eventRecords->Length();
+    for (uint32_t i = 0; i < len; ++i) {
+      n += (*eventRecords)[i].SizeOfExcludingThis(aMallocSizeOf);
+    }
   }
 
   n += gEventNameIDMap.ShallowSizeOfExcludingThis(aMallocSizeOf);
   for (auto iter = gEventNameIDMap.ConstIter(); !iter.Done(); iter.Next()) {
     n += iter.Key().SizeOfExcludingThisIfUnshared(aMallocSizeOf);
   }
 
   n += gCategoryNameIDMap.ShallowSizeOfExcludingThis(aMallocSizeOf);
--- a/toolkit/components/telemetry/TelemetryEvent.h
+++ b/toolkit/components/telemetry/TelemetryEvent.h
@@ -3,16 +3,22 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #ifndef TelemetryEvent_h__
 #define TelemetryEvent_h__
 
 #include "mozilla/TelemetryEventEnums.h"
 
+namespace mozilla {
+namespace Telemetry {
+  struct ChildEventData;
+}
+}
+
 // This module is internal to Telemetry. It encapsulates Telemetry's
 // event recording and storage logic. It should only be used by
 // Telemetry.cpp. These functions should not be used anywhere else.
 // For the public interface to Telemetry functionality, see Telemetry.h.
 
 namespace TelemetryEvent {
 
 void InitializeGlobalState(bool canRecordBase, bool canRecordExtended);
@@ -25,16 +31,20 @@ void SetCanRecordExtended(bool b);
 nsresult RecordEvent(const nsACString& aCategory, const nsACString& aMethod,
                      const nsACString& aObject, JS::HandleValue aValue,
                      JS::HandleValue aExtra, JSContext* aCx,
                      uint8_t optional_argc);
 void SetEventRecordingEnabled(const nsACString& aCategory, bool aEnabled);
 nsresult CreateSnapshots(uint32_t aDataset, bool aClear, JSContext* aCx,
                          uint8_t optional_argc, JS::MutableHandleValue aResult);
 
+// Record events from child processes.
+nsresult RecordChildEvents(GeckoProcessType aProcessType,
+                           const nsTArray<mozilla::Telemetry::ChildEventData>& aEvents);
+
 // Only to be used for testing.
 void ClearEvents();
 
 size_t SizeOfIncludingThis(mozilla::MallocSizeOf aMallocSizeOf);
 
 } // namespace TelemetryEvent
 
 #endif // TelemetryEvent_h__
--- a/toolkit/components/telemetry/TelemetrySession.jsm
+++ b/toolkit/components/telemetry/TelemetrySession.jsm
@@ -1023,25 +1023,27 @@ var Impl = {
 
   getEvents(isSubsession, clearSubsession) {
     if (!isSubsession) {
       // We only support scalars for subsessions.
       this._log.trace("getEvents - We only support events in subsessions.");
       return [];
     }
 
-    let events = Telemetry.snapshotBuiltinEvents(this.getDatasetType(),
-                                                 clearSubsession);
+    let snapshot = Telemetry.snapshotBuiltinEvents(this.getDatasetType(),
+                                                   clearSubsession);
 
     // Don't return the test events outside of test environments.
     if (!this._testing) {
-      events = events.filter(e => !e[1].startsWith("telemetry.test"));
+      for (let proc of Object.keys(snapshot)) {
+        snapshot[proc] = snapshot[proc].filter(e => !e[1].startsWith("telemetry.test"));
+      }
     }
 
-    return events;
+    return snapshot;
   },
 
   getThreadHangStats: function getThreadHangStats(stats) {
     stats.forEach((thread) => {
       thread.activity = this.packHistogram(thread.activity);
       thread.hangs.forEach((hang) => {
         hang.histogram = this.packHistogram(hang.histogram);
       });
@@ -1291,43 +1293,46 @@ var Impl = {
       return payloadObj;
     }
 
     // Additional payload for chrome process.
     let histograms = protect(() => this.getHistograms(isSubsession, clearSubsession), {});
     let keyedHistograms = protect(() => this.getKeyedHistograms(isSubsession, clearSubsession), {});
     let scalars = protect(() => this.getScalars(isSubsession, clearSubsession), {});
     let keyedScalars = protect(() => this.getScalars(isSubsession, clearSubsession, true), {});
+    let events = protect(() => this.getEvents(isSubsession, clearSubsession))
 
     payloadObj.histograms = histograms[HISTOGRAM_SUFFIXES.PARENT] || {};
     payloadObj.keyedHistograms = keyedHistograms[HISTOGRAM_SUFFIXES.PARENT] || {};
     payloadObj.processes = {
       parent: {
         scalars: scalars[INTERNAL_PROCESSES_NAMES.PARENT] || {},
         keyedScalars: keyedScalars[INTERNAL_PROCESSES_NAMES.PARENT] || {},
-        events: protect(() => this.getEvents(isSubsession, clearSubsession)),
+        events: events[INTERNAL_PROCESSES_NAMES.PARENT] || [],
       },
       content: {
         scalars: scalars[INTERNAL_PROCESSES_NAMES.CONTENT],
         keyedScalars: keyedScalars[INTERNAL_PROCESSES_NAMES.CONTENT],
         histograms: histograms[HISTOGRAM_SUFFIXES.CONTENT],
         keyedHistograms: keyedHistograms[HISTOGRAM_SUFFIXES.CONTENT],
+        events: events[INTERNAL_PROCESSES_NAMES.CONTENT] || [],
       },
     };
 
     // Only include the GPU process if we've accumulated data for it.
     if (HISTOGRAM_SUFFIXES.GPU in histograms ||
         HISTOGRAM_SUFFIXES.GPU in keyedHistograms ||
         INTERNAL_PROCESSES_NAMES.GPU in scalars ||
         INTERNAL_PROCESSES_NAMES.GPU in keyedScalars) {
       payloadObj.processes.gpu = {
         scalars: scalars[INTERNAL_PROCESSES_NAMES.GPU],
         keyedScalars: keyedScalars[INTERNAL_PROCESSES_NAMES.GPU],
         histograms: histograms[HISTOGRAM_SUFFIXES.GPU],
         keyedHistograms: keyedHistograms[HISTOGRAM_SUFFIXES.GPU],
+        events: events[INTERNAL_PROCESSES_NAMES.GPU] || [],
       };
     }
 
     payloadObj.info = info;
 
     // Add extended set measurements for chrome process.
     if (Telemetry.canRecordExtended) {
       payloadObj.slowSQL = protect(() => Telemetry.slowSQL);
--- a/toolkit/components/telemetry/ipc/TelemetryComms.h
+++ b/toolkit/components/telemetry/ipc/TelemetryComms.h
@@ -54,16 +54,30 @@ struct KeyedScalarAction
   ScalarID mId;
   ScalarActionType mActionType;
   nsCString mKey;
   // We need to wrap mData in a Maybe otherwise the IPC system
   // is unable to instantiate a ScalarAction.
   Maybe<ScalarVariant> mData;
 };
 
+struct EventExtraEntry {
+  nsCString key;
+  nsCString value;
+};
+
+struct ChildEventData {
+  double timestamp;
+  nsCString category;
+  nsCString method;
+  nsCString object;
+  mozilla::Maybe<nsCString> value;
+  nsTArray<EventExtraEntry> extra;
+};
+
 } // namespace Telemetry
 } // namespace mozilla
 
 namespace IPC {
 
 template<>
 struct
 ParamTraits<mozilla::Telemetry::Accumulation>
@@ -283,11 +297,65 @@ ParamTraits<mozilla::Telemetry::KeyedSca
         MOZ_ASSERT(false, "Unknown keyed scalar type.");
         return false;
     }
 
     return true;
   }
 };
 
+template<>
+struct
+ParamTraits<mozilla::Telemetry::ChildEventData>
+{
+  typedef mozilla::Telemetry::ChildEventData paramType;
+
+  static void Write(Message* aMsg, const paramType& aParam)
+  {
+    WriteParam(aMsg, aParam.timestamp);
+    WriteParam(aMsg, aParam.category);
+    WriteParam(aMsg, aParam.method);
+    WriteParam(aMsg, aParam.object);
+    WriteParam(aMsg, aParam.value);
+    WriteParam(aMsg, aParam.extra);
+  }
+
+  static bool Read(const Message* aMsg, PickleIterator* aIter, paramType* aResult)
+  {
+    if (!ReadParam(aMsg, aIter, &(aResult->timestamp)) ||
+        !ReadParam(aMsg, aIter, &(aResult->category)) ||
+        !ReadParam(aMsg, aIter, &(aResult->method)) ||
+        !ReadParam(aMsg, aIter, &(aResult->object)) ||
+        !ReadParam(aMsg, aIter, &(aResult->value)) ||
+        !ReadParam(aMsg, aIter, &(aResult->extra))) {
+      return false;
+    }
+
+    return true;
+  }
+};
+
+template<>
+struct
+ParamTraits<mozilla::Telemetry::EventExtraEntry>
+{
+  typedef mozilla::Telemetry::EventExtraEntry paramType;
+
+  static void Write(Message* aMsg, const paramType& aParam)
+  {
+    WriteParam(aMsg, aParam.key);
+    WriteParam(aMsg, aParam.value);
+  }
+
+  static bool Read(const Message* aMsg, PickleIterator* aIter, paramType* aResult)
+  {
+    if (!ReadParam(aMsg, aIter, &(aResult->key)) ||
+        !ReadParam(aMsg, aIter, &(aResult->value))) {
+      return false;
+    }
+
+    return true;
+  }
+};
+
 } // namespace IPC
 
 #endif // Telemetry_Comms_h__
--- a/toolkit/components/telemetry/ipc/TelemetryIPC.cpp
+++ b/toolkit/components/telemetry/ipc/TelemetryIPC.cpp
@@ -2,16 +2,17 @@
 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #include "TelemetryIPC.h"
 #include "../TelemetryScalar.h"
 #include "../TelemetryHistogram.h"
+#include "../TelemetryEvent.h"
 
 namespace mozilla {
 
 void
 TelemetryIPC::AccumulateChildHistograms(GeckoProcessType aProcessType,
                                         const nsTArray<Telemetry::Accumulation>& aAccumulations)
 {
   TelemetryHistogram::AccumulateChild(aProcessType, aAccumulations);
@@ -33,9 +34,15 @@ TelemetryIPC::UpdateChildScalars(GeckoPr
 
 void
 TelemetryIPC::UpdateChildKeyedScalars(GeckoProcessType aProcessType,
                                       const nsTArray<Telemetry::KeyedScalarAction>& aScalarActions)
 {
   TelemetryScalar::UpdateChildKeyedData(aProcessType, aScalarActions);
 }
 
+void
+TelemetryIPC::RecordChildEvents(GeckoProcessType aProcessType, const nsTArray<Telemetry::ChildEventData>& aEvents)
+{
+  TelemetryEvent::RecordChildEvents(aProcessType, aEvents);
 }
+
+}
--- a/toolkit/components/telemetry/ipc/TelemetryIPC.h
+++ b/toolkit/components/telemetry/ipc/TelemetryIPC.h
@@ -15,45 +15,58 @@
 
 namespace mozilla {
 namespace Telemetry {
 
 struct Accumulation;
 struct KeyedAccumulation;
 struct ScalarAction;
 struct KeyedScalarAction;
+struct ChildEventData;
 
 }
 
 namespace TelemetryIPC {
 
 /**
  * Accumulate child process data into histograms for the given process type.
  *
+ * @param aProcessType - the process type to accumulate the histograms for
  * @param aAccumulations - accumulation actions to perform
  */
 void AccumulateChildHistograms(GeckoProcessType aProcessType, const nsTArray<Telemetry::Accumulation>& aAccumulations);
 
 /**
  * Accumulate child process data into keyed histograms for the given process type.
  *
+ * @param aProcessType - the process type to accumulate the keyed histograms for
  * @param aAccumulations - accumulation actions to perform
  */
 void AccumulateChildKeyedHistograms(GeckoProcessType aProcessType, const nsTArray<Telemetry::KeyedAccumulation>& aAccumulations);
 
 /**
  * Update scalars for the given process type with the data coming from child process.
  *
+ * @param aProcessType - the process type to process the scalar actions for
  * @param aScalarActions - actions to update the scalar data
  */
 void UpdateChildScalars(GeckoProcessType aProcessType, const nsTArray<Telemetry::ScalarAction>& aScalarActions);
 
 /**
- * Update keyed  scalars for the given process type with the data coming from child process.
+ * Update keyed scalars for the given process type with the data coming from child process.
  *
+ * @param aProcessType - the process type to process the keyed scalar actions for
  * @param aScalarActions - actions to update the keyed scalar data
  */
 void UpdateChildKeyedScalars(GeckoProcessType aProcessType, const nsTArray<Telemetry::KeyedScalarAction>& aScalarActions);
 
+/**
+ * Record events for the given process type with the data coming from child process.
+ *
+ * @param aProcessType - the process type to record the events for
+ * @param aEvents - events to record
+ */
+void RecordChildEvents(GeckoProcessType aProcessType, const nsTArray<Telemetry::ChildEventData>& aEvents);
+
 }
 }
 
 #endif // TelemetryIPC_h__
--- a/toolkit/components/telemetry/ipc/TelemetryIPCAccumulator.cpp
+++ b/toolkit/components/telemetry/ipc/TelemetryIPCAccumulator.cpp
@@ -22,52 +22,55 @@ using mozilla::StaticMutex;
 using mozilla::StaticMutexAutoLock;
 using mozilla::StaticAutoPtr;
 using mozilla::Telemetry::Accumulation;
 using mozilla::Telemetry::KeyedAccumulation;
 using mozilla::Telemetry::ScalarActionType;
 using mozilla::Telemetry::ScalarAction;
 using mozilla::Telemetry::KeyedScalarAction;
 using mozilla::Telemetry::ScalarVariant;
+using mozilla::Telemetry::ChildEventData;
 
 namespace TelemetryIPCAccumulator = mozilla::TelemetryIPCAccumulator;
 
 // Sending each remote accumulation immediately places undue strain on the
 // IPC subsystem. Batch the remote accumulations for a period of time before
 // sending them all at once. This value was chosen as a balance between data
 // timeliness and performance (see bug 1218576)
 const uint32_t kBatchTimeoutMs = 2000;
 
 // To stop growing unbounded in memory while waiting for kBatchTimeoutMs to
 // drain the g*Accumulations arrays, request an immediate flush if the arrays
 // manage to reach this high water mark of elements.
 const size_t kHistogramAccumulationsArrayHighWaterMark = 5 * 1024;
 
-// For batching and sending child process accumulations to the parent
+// This timer is used for batching and sending child process accumulations to the parent.
 nsITimer* gIPCTimer = nullptr;
 mozilla::Atomic<bool, mozilla::Relaxed> gIPCTimerArmed(false);
 mozilla::Atomic<bool, mozilla::Relaxed> gIPCTimerArming(false);
 
-// For batching and sending child process accumulations to the parent
+// This batches child process accumulations that should be sent to the parent.
 StaticAutoPtr<nsTArray<Accumulation>> gHistogramAccumulations;
 StaticAutoPtr<nsTArray<KeyedAccumulation>> gKeyedHistogramAccumulations;
 StaticAutoPtr<nsTArray<ScalarAction>> gChildScalarsActions;
 StaticAutoPtr<nsTArray<KeyedScalarAction>> gChildKeyedScalarsActions;
+StaticAutoPtr<nsTArray<ChildEventData>> gChildEvents;
 
 // This is a StaticMutex rather than a plain Mutex so that (1)
 // it gets initialised in a thread-safe manner the first time
 // it is used, and (2) because it is never de-initialised, and
 // a normal Mutex would show up as a leak in BloatView.  StaticMutex
 // also has the "OffTheBooks" property, so it won't show as a leak
 // in BloatView.
 static StaticMutex gTelemetryIPCAccumulatorMutex;
 
 namespace {
 
-void DoArmIPCTimerMainThread(const StaticMutexAutoLock& lock)
+void
+DoArmIPCTimerMainThread(const StaticMutexAutoLock& lock)
 {
   MOZ_ASSERT(NS_IsMainThread());
   gIPCTimerArming = false;
   if (gIPCTimerArmed) {
     return;
   }
   if (!gIPCTimer) {
     CallCreateInstance(NS_TIMER_CONTRACTID, &gIPCTimer);
@@ -76,17 +79,18 @@ void DoArmIPCTimerMainThread(const Stati
     gIPCTimer->InitWithNamedFuncCallback(TelemetryIPCAccumulator::IPCTimerFired,
                                          nullptr, kBatchTimeoutMs,
                                          nsITimer::TYPE_ONE_SHOT,
                                          "TelemetryIPCAccumulator::IPCTimerFired");
     gIPCTimerArmed = true;
   }
 }
 
-void ArmIPCTimer(const StaticMutexAutoLock& lock)
+void
+ArmIPCTimer(const StaticMutexAutoLock& lock)
 {
   if (gIPCTimerArmed || gIPCTimerArming) {
     return;
   }
   gIPCTimerArming = true;
   if (NS_IsMainThread()) {
     DoArmIPCTimerMainThread(lock);
   } else {
@@ -166,43 +170,68 @@ TelemetryIPCAccumulator::RecordChildKeye
     gChildKeyedScalarsActions = new nsTArray<KeyedScalarAction>();
   }
   // Store the action.
   gChildKeyedScalarsActions->AppendElement(
     KeyedScalarAction{aId, aAction, NS_ConvertUTF16toUTF8(aKey), Some(aValue)});
   ArmIPCTimer(locker);
 }
 
+void
+TelemetryIPCAccumulator::RecordChildEvent(double timestamp,
+                                          const nsACString& category,
+                                          const nsACString& method,
+                                          const nsACString& object,
+                                          const mozilla::Maybe<nsCString>& value,
+                                          const nsTArray<mozilla::Telemetry::EventExtraEntry>& extra)
+{
+  StaticMutexAutoLock locker(gTelemetryIPCAccumulatorMutex);
+  if (!gChildEvents) {
+    gChildEvents = new nsTArray<ChildEventData>();
+  }
+  // Store the action.
+  gChildEvents->AppendElement(ChildEventData{timestamp, nsCString(category),
+                                             nsCString(method), nsCString(object),
+                                             value,
+                                             nsTArray<mozilla::Telemetry::EventExtraEntry>(extra)});
+  ArmIPCTimer(locker);
+}
+
 // This method takes the lock only to double-buffer the batched telemetry.
 // It releases the lock before calling out to IPC code which can (and does)
 // Accumulate (which would deadlock)
 template<class TActor>
 static void
 SendAccumulatedData(TActor* ipcActor)
 {
   // Get the accumulated data and free the storage buffers.
   nsTArray<Accumulation> accumulationsToSend;
   nsTArray<KeyedAccumulation> keyedAccumulationsToSend;
   nsTArray<ScalarAction> scalarsToSend;
   nsTArray<KeyedScalarAction> keyedScalarsToSend;
+  nsTArray<ChildEventData> eventsToSend;
+
   {
     StaticMutexAutoLock locker(gTelemetryIPCAccumulatorMutex);
     if (gHistogramAccumulations) {
       accumulationsToSend.SwapElements(*gHistogramAccumulations);
     }
     if (gKeyedHistogramAccumulations) {
       keyedAccumulationsToSend.SwapElements(*gKeyedHistogramAccumulations);
     }
     // Copy the scalar actions.
     if (gChildScalarsActions) {
       scalarsToSend.SwapElements(*gChildScalarsActions);
     }
     if (gChildKeyedScalarsActions) {
       keyedScalarsToSend.SwapElements(*gChildKeyedScalarsActions);
     }
+    if (gChildEvents) {
+      eventsToSend.SwapElements(*gChildEvents);
+    }
   }
 
   // Send the accumulated data to the parent process.
   mozilla::Unused << NS_WARN_IF(!ipcActor);
   if (accumulationsToSend.Length()) {
     mozilla::Unused <<
       NS_WARN_IF(!ipcActor->SendAccumulateChildHistograms(accumulationsToSend));
   }
@@ -213,16 +242,20 @@ SendAccumulatedData(TActor* ipcActor)
   if (scalarsToSend.Length()) {
     mozilla::Unused <<
       NS_WARN_IF(!ipcActor->SendUpdateChildScalars(scalarsToSend));
   }
   if (keyedScalarsToSend.Length()) {
     mozilla::Unused <<
       NS_WARN_IF(!ipcActor->SendUpdateChildKeyedScalars(keyedScalarsToSend));
   }
+  if (eventsToSend.Length()) {
+    mozilla::Unused <<
+      NS_WARN_IF(!ipcActor->SendRecordChildEvents(eventsToSend));
+  }
 }
 
 
 // To ensure we don't loop IPCTimerFired->AccumulateChild->arm timer, we don't
 // unset gIPCTimerArmed until the IPC completes
 //
 // This function must be called on the main thread, otherwise IPC will fail.
 void
@@ -255,16 +288,17 @@ TelemetryIPCAccumulator::DeInitializeGlo
   if (gIPCTimer) {
     NS_RELEASE(gIPCTimer);
   }
 
   gHistogramAccumulations = nullptr;
   gKeyedHistogramAccumulations = nullptr;
   gChildScalarsActions = nullptr;
   gChildKeyedScalarsActions = nullptr;
+  gChildEvents = nullptr;
 }
 
 void
 TelemetryIPCAccumulator::DispatchToMainThread(already_AddRefed<nsIRunnable>&& aEvent)
 {
   nsCOMPtr<nsIRunnable> event(aEvent);
   nsCOMPtr<nsIThread> thread;
   nsresult rv = NS_GetMainThread(getter_AddRefs(thread));
--- a/toolkit/components/telemetry/ipc/TelemetryIPCAccumulator.h
+++ b/toolkit/components/telemetry/ipc/TelemetryIPCAccumulator.h
@@ -2,16 +2,17 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #ifndef TelemetryIPCAccumulator_h__
 #define TelemetryIPCAccumulator_h__
 
 #include "mozilla/AlreadyAddRefed.h"
+#include "mozilla/Maybe.h"
 #include "TelemetryComms.h"
 
 class nsIRunnable;
 class nsITimer;
 class nsAString;
 class nsCString;
 
 namespace mozilla {
@@ -26,16 +27,23 @@ void AccumulateChildKeyedHistogram(mozil
 void RecordChildScalarAction(mozilla::Telemetry::ScalarID aId,
                              mozilla::Telemetry::ScalarActionType aAction,
                              const mozilla::Telemetry::ScalarVariant& aValue);
 
 void RecordChildKeyedScalarAction(mozilla::Telemetry::ScalarID aId, const nsAString& aKey,
                                   mozilla::Telemetry::ScalarActionType aAction,
                                   const mozilla::Telemetry::ScalarVariant& aValue);
 
+void RecordChildEvent(double timestamp,
+                      const nsACString& category,
+                      const nsACString& method,
+                      const nsACString& object,
+                      const mozilla::Maybe<nsCString>& value,
+                      const nsTArray<mozilla::Telemetry::EventExtraEntry>& extra);
+
 void IPCTimerFired(nsITimer* aTimer, void* aClosure);
 void DeInitializeGlobalState();
 
 void DispatchToMainThread(already_AddRefed<nsIRunnable>&& aEvent);
 
 }
 }
 
new file mode 100644
--- /dev/null
+++ b/toolkit/components/telemetry/tests/unit/test_ChildEvents.js
@@ -0,0 +1,90 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/
+*/
+
+Cu.import("resource://gre/modules/Services.jsm", this);
+Cu.import("resource://gre/modules/TelemetryController.jsm", this);
+Cu.import("resource://gre/modules/TelemetrySession.jsm", this);
+Cu.import("resource://gre/modules/PromiseUtils.jsm", this);
+Cu.import("resource://testing-common/ContentTaskUtils.jsm", this);
+
+const MESSAGE_CHILD_TEST_DONE = "ChildTest:Done";
+
+const PLATFORM_VERSION = "1.9.2";
+const APP_VERSION = "1";
+const APP_ID = "xpcshell@tests.mozilla.org";
+const APP_NAME = "XPCShell";
+
+const RECORDED_CONTENT_EVENTS = [
+  ["telemetry.test", "test1", "object1"],
+  ["telemetry.test", "test1", "object1", null, {"key1": "x", "key2": "y"}],
+  ["telemetry.test", "test1", "object1", "foo", {"key1": "x", "key2": "y"}],
+];
+
+function run_child_test() {
+  // Record some events in the "content" process.
+  RECORDED_CONTENT_EVENTS.forEach(e => Telemetry.recordEvent(...e));
+}
+
+/**
+ * This function waits until content events are reported into the
+ * events snapshot.
+ */
+function* waitForContentEvents() {
+  yield ContentTaskUtils.waitForCondition(() => {
+    const snapshot =
+      Telemetry.snapshotBuiltinEvents(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN, false);
+    return Object.keys(snapshot).includes("tab");
+  });
+}
+
+add_task(function*() {
+  if (!runningInParent) {
+    TelemetryController.testSetupContent();
+    run_child_test();
+    do_send_remote_message(MESSAGE_CHILD_TEST_DONE);
+    return;
+  }
+
+  // Setup.
+  do_get_profile(true);
+  loadAddonManager(APP_ID, APP_NAME, APP_VERSION, PLATFORM_VERSION);
+  Services.prefs.setBoolPref(PREF_TELEMETRY_ENABLED, true);
+  yield TelemetryController.testSetup();
+  // Make sure we don't generate unexpected pings due to pref changes.
+  yield setEmptyPrefWatchlist();
+  // Enable recording for the test event category.
+  Telemetry.setEventRecordingEnabled("telemetry.test", true);
+
+  // Run test in child, don't wait for it to finish: just wait for the
+  // MESSAGE_CHILD_TEST_DONE.
+  run_test_in_child("test_ChildEvents.js");
+  yield do_await_remote_message(MESSAGE_CHILD_TEST_DONE);
+
+  // Once events are set by the content process, they don't immediately get
+  // sent to the parent process. Wait for the Telemetry IPC Timer to trigger
+  // and batch send the data back to the parent process.
+  yield waitForContentEvents();
+
+  // Get an "environment-changed" ping rather than a "test-ping", as
+  // event measurements are only supported in subsession pings.
+  const payload = TelemetrySession.getPayload("environment-change");
+
+  // Validate the event data.
+  Assert.ok("processes" in payload, "Should have processes section");
+  Assert.ok("parent" in payload.processes, "Should have main process section");
+  Assert.ok("events" in payload.processes.parent, "Main process section should have events.");
+  Assert.ok("content" in payload.processes, "Should have child process section");
+  Assert.ok("events" in payload.processes.content, "Child process section should have events.");
+
+  let events = payload.processes.content.events.map(e => e.slice(1));
+  Assert.deepEqual(events, RECORDED_CONTENT_EVENTS, "Should have recorded content events.");
+
+  // Make sure all events are cleared from storage properly.
+  let snapshot =
+      Telemetry.snapshotBuiltinEvents(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN, true);
+  Assert.equal(Object.keys(snapshot).length, 1, "Should have events from one process.");
+  snapshot =
+      Telemetry.snapshotBuiltinEvents(Ci.nsITelemetry.DATASET_RELEASE_CHANNEL_OPTIN, true);
+  Assert.equal(Object.keys(snapshot).length, 0, "Should have cleared all events from storage.");
+});
--- a/toolkit/components/telemetry/tests/unit/test_TelemetryEvents.js
+++ b/toolkit/components/telemetry/tests/unit/test_TelemetryEvents.js
@@ -40,39 +40,42 @@ add_task(function* test_recording_state(
   const events = [
     ["telemetry.test", "test1", "object1"],
     ["telemetry.test.second", "test", "object1"],
   ];
 
   // Both test categories should be off by default.
   events.forEach(e => Telemetry.recordEvent(...e));
   let snapshot = Telemetry.snapshotBuiltinEvents(OPTIN, true);
-  Assert.deepEqual(snapshot, [], "Should not have recorded any events.");
+  Assert.equal(Object.keys(snapshot).length, 0, "Should not have recorded any events.");
 
   // Enable one test category and see that we record correctly.
   Telemetry.setEventRecordingEnabled("telemetry.test", true);
   events.forEach(e => Telemetry.recordEvent(...e));
   snapshot = Telemetry.snapshotBuiltinEvents(OPTIN, true);
-  Assert.equal(snapshot.length, 1, "Should have recorded one event.");
-  Assert.equal(snapshot[0][1], "telemetry.test", "Should have recorded one event in telemetry.test");
+  Assert.ok(("default" in snapshot), "Should have entry for main process.");
+  Assert.equal(snapshot.default.length, 1, "Should have recorded one event.");
+  Assert.equal(snapshot.default[0][1], "telemetry.test", "Should have recorded one event in telemetry.test");
 
   // Also enable the other test category and see that we record correctly.
   Telemetry.setEventRecordingEnabled("telemetry.test.second", true);
   events.forEach(e => Telemetry.recordEvent(...e));
   snapshot = Telemetry.snapshotBuiltinEvents(OPTIN, true);
-  Assert.equal(snapshot.length, 2, "Should have recorded two events.");
-  Assert.equal(snapshot[0][1], "telemetry.test", "Should have recorded one event in telemetry.test");
-  Assert.equal(snapshot[1][1], "telemetry.test.second", "Should have recorded one event in telemetry.test.second");
+  Assert.ok(("default" in snapshot), "Should have entry for main process.");
+  Assert.equal(snapshot.default.length, 2, "Should have recorded two events.");
+  Assert.equal(snapshot.default[0][1], "telemetry.test", "Should have recorded one event in telemetry.test");
+  Assert.equal(snapshot.default[1][1], "telemetry.test.second", "Should have recorded one event in telemetry.test.second");
 
   // Now turn of one category again and check that this works as expected.
   Telemetry.setEventRecordingEnabled("telemetry.test", false);
   events.forEach(e => Telemetry.recordEvent(...e));
   snapshot = Telemetry.snapshotBuiltinEvents(OPTIN, true);
-  Assert.equal(snapshot.length, 1, "Should have recorded one event.");
-  Assert.equal(snapshot[0][1], "telemetry.test.second", "Should have recorded one event in telemetry.test.second");
+  Assert.ok(("default" in snapshot), "Should have entry for main process.");
+  Assert.equal(snapshot.default.length, 1, "Should have recorded one event.");
+  Assert.equal(snapshot.default[0][1], "telemetry.test.second", "Should have recorded one event in telemetry.test.second");
 });
 
 add_task(function* recording_setup() {
   // Make sure both test categories are enabled for the remaining tests.
   // Otherwise their event recording won't work.
   Telemetry.setEventRecordingEnabled("telemetry.test", true);
   Telemetry.setEventRecordingEnabled("telemetry.test.second", true);
 });
@@ -137,99 +140,105 @@ add_task(function* test_recording() {
 
       let recordedData = events[i].slice(1);
       let expectedData = expectedEvents[i].event.slice();
       Assert.deepEqual(recordedData, expectedData, "The recorded event data should match.");
     }
   };
 
   // Check that the expected events were recorded.
-  let events = Telemetry.snapshotBuiltinEvents(OPTIN, false);
-  checkEvents(events, expected);
+  let snapshot = Telemetry.snapshotBuiltinEvents(OPTIN, false);
+  Assert.ok(("default" in snapshot), "Should have entry for main process.");
+  checkEvents(snapshot.default, expected);
 
   // Check serializing only opt-out events.
-  events = Telemetry.snapshotBuiltinEvents(OPTOUT, false);
+  snapshot = Telemetry.snapshotBuiltinEvents(OPTOUT, false);
+  Assert.ok(("default" in snapshot), "Should have entry for main process.");
   let filtered = expected.filter(e => e.optout == true);
-  checkEvents(events, filtered);
+  checkEvents(snapshot.default, filtered);
 });
 
 add_task(function* test_clear() {
   Telemetry.clearEvents();
 
   const COUNT = 10;
   for (let i = 0; i < COUNT; ++i) {
     Telemetry.recordEvent("telemetry.test", "test1", "object1");
     Telemetry.recordEvent("telemetry.test.second", "test", "object1");
   }
 
   // Check that events were recorded.
   // The events are cleared by passing the respective flag.
-  let events = Telemetry.snapshotBuiltinEvents(OPTIN, true);
-  Assert.equal(events.length, 2 * COUNT, `Should have recorded ${2 * COUNT} events.`);
+  let snapshot = Telemetry.snapshotBuiltinEvents(OPTIN, true);
+  Assert.ok(("default" in snapshot), "Should have entry for main process.");
+  Assert.equal(snapshot.default.length, 2 * COUNT, `Should have recorded ${2 * COUNT} events.`);
 
   // Now the events should be cleared.
-  events = Telemetry.snapshotBuiltinEvents(OPTIN, false);
-  Assert.equal(events.length, 0, `Should have cleared the events.`);
+  snapshot = Telemetry.snapshotBuiltinEvents(OPTIN, false);
+  Assert.equal(Object.keys(snapshot).length, 0, `Should have cleared the events.`);
 });
 
 add_task(function* test_expiry() {
   Telemetry.clearEvents();
 
   // Recording call with event that is expired by version.
   Telemetry.recordEvent("telemetry.test", "expired_version", "object1");
-  let events = Telemetry.snapshotBuiltinEvents(OPTIN, true);
-  Assert.equal(events.length, 0, "Should not record event with expired version.");
+  let snapshot = Telemetry.snapshotBuiltinEvents(OPTIN, true);
+  Assert.equal(Object.keys(snapshot).length, 0, "Should not record event with expired version.");
 
   // Recording call with event that is expired by date.
   Telemetry.recordEvent("telemetry.test", "expired_date", "object1");
-  events = Telemetry.snapshotBuiltinEvents(OPTIN, true);
-  Assert.equal(events.length, 0, "Should not record event with expired date.");
+  snapshot = Telemetry.snapshotBuiltinEvents(OPTIN, true);
+  Assert.equal(Object.keys(snapshot).length, 0, "Should not record event with expired date.");
 
   // Recording call with event that has expiry_version and expiry_date in the future.
   Telemetry.recordEvent("telemetry.test", "not_expired_optout", "object1");
-  events = Telemetry.snapshotBuiltinEvents(OPTOUT, true);
-  Assert.equal(events.length, 1, "Should record event when date and version are not expired.");
+  snapshot = Telemetry.snapshotBuiltinEvents(OPTOUT, true);
+  Assert.ok(("default" in snapshot), "Should have entry for main process.");
+  Assert.equal(snapshot.default.length, 1, "Should record event when date and version are not expired.");
 });
 
 add_task(function* test_invalidParams() {
   Telemetry.clearEvents();
 
   // Recording call with wrong type for value argument.
   Telemetry.recordEvent("telemetry.test", "test1", "object1", 1);
-  let events = Telemetry.snapshotBuiltinEvents(OPTIN, true);
-  Assert.equal(events.length, 0, "Should not record event when value argument with invalid type is passed.");
+  let snapshot = Telemetry.snapshotBuiltinEvents(OPTIN, true);
+  Assert.equal(Object.keys(snapshot).length, 0, "Should not record event when value argument with invalid type is passed.");
 
   // Recording call with wrong type for extra argument.
   Telemetry.recordEvent("telemetry.test", "test1", "object1", null, "invalid");
-  events = Telemetry.snapshotBuiltinEvents(OPTIN, true);
-  Assert.equal(events.length, 0, "Should not record event when extra argument with invalid type is passed.");
+  snapshot = Telemetry.snapshotBuiltinEvents(OPTIN, true);
+  Assert.equal(Object.keys(snapshot).length, 0, "Should not record event when extra argument with invalid type is passed.");
 
   // Recording call with unknown extra key.
   Telemetry.recordEvent("telemetry.test", "test1", "object1", null, {"key3": "x"});
-  events = Telemetry.snapshotBuiltinEvents(OPTIN, true);
-  Assert.equal(events.length, 0, "Should not record event when extra argument with invalid key is passed.");
+  snapshot = Telemetry.snapshotBuiltinEvents(OPTIN, true);
+  Assert.equal(Object.keys(snapshot).length, 0, "Should not record event when extra argument with invalid key is passed.");
 
   // Recording call with invalid value type.
   Telemetry.recordEvent("telemetry.test", "test1", "object1", null, {"key3": 1});
-  events = Telemetry.snapshotBuiltinEvents(OPTIN, true);
-  Assert.equal(events.length, 0, "Should not record event when extra argument with invalid value type is passed.");
+  snapshot = Telemetry.snapshotBuiltinEvents(OPTIN, true);
+  Assert.equal(Object.keys(snapshot).length, 0, "Should not record event when extra argument with invalid value type is passed.");
 });
 
 add_task(function* test_storageLimit() {
   Telemetry.clearEvents();
 
   // Record more events than the storage limit allows.
   let LIMIT = 1000;
   let COUNT = LIMIT + 10;
   for (let i = 0; i < COUNT; ++i) {
     Telemetry.recordEvent("telemetry.test", "test1", "object1", String(i));
   }
 
   // Check that the right events were recorded.
-  let events = Telemetry.snapshotBuiltinEvents(OPTIN, true);
+  let snapshot = Telemetry.snapshotBuiltinEvents(OPTIN, true);
+  Assert.ok(("default" in snapshot), "Should have entry for main process.");
+  let events = snapshot.default;
   Assert.equal(events.length, LIMIT, `Should have only recorded ${LIMIT} events`);
   Assert.ok(events.every((e, idx) => e[4] === String(idx)),
             "Should have recorded all events from before hitting the limit.");
 });
 
 add_task(function* test_valueLimits() {
   Telemetry.clearEvents();
 
@@ -260,17 +269,19 @@ add_task(function* test_valueLimits() {
   // Strip off trailing null values to match the serialized events.
   for (let e of expected) {
     while ((e.length >= 3) && (e[e.length - 1] === null)) {
       e.pop();
     }
   }
 
   // Check that the right events were recorded.
-  let events = Telemetry.snapshotBuiltinEvents(OPTIN, true);
+  let snapshot = Telemetry.snapshotBuiltinEvents(OPTIN, true);
+  Assert.ok(("default" in snapshot), "Should have entry for main process.");
+  let events = snapshot.default;
   Assert.equal(events.length, expected.length,
                "Should have recorded the expected number of events");
   for (let i = 0; i < expected.length; ++i) {
     Assert.deepEqual(events[i].slice(1), expected[i],
                      "Should have recorded the expected event data.");
   }
 });
 
@@ -278,13 +289,15 @@ add_task(function* test_unicodeValues() 
   Telemetry.clearEvents();
 
   // Record string values containing unicode characters.
   let value = "漢語";
   Telemetry.recordEvent("telemetry.test", "test1", "object1", value);
   Telemetry.recordEvent("telemetry.test", "test1", "object1", null, {"key1": value});
 
   // Check that the values were correctly recorded.
-  let events = Telemetry.snapshotBuiltinEvents(OPTIN, true);
+  let snapshot = Telemetry.snapshotBuiltinEvents(OPTIN, true);
+  Assert.ok(("default" in snapshot), "Should have entry for main process.");
+  let events = snapshot.default;
   Assert.equal(events.length, 2, "Should have recorded 2 events.");
   Assert.equal(events[0][4], value, "Should have recorded the right value.");
   Assert.equal(events[1][5]["key1"], value, "Should have recorded the right extra value.");
 });
--- a/toolkit/components/telemetry/tests/unit/xpcshell.ini
+++ b/toolkit/components/telemetry/tests/unit/xpcshell.ini
@@ -51,22 +51,24 @@ tags = addons
 skip-if = os == "android" # Disabled due to intermittent orange on Android
 tags = addons
 [test_TelemetrySession.js]
 tags = addons
 [test_ThreadHangStats.js]
 run-sequentially = Bug 1046307, test can fail intermittently when CPU load is high
 [test_TelemetrySend.js]
 [test_ChildHistograms.js]
-skip-if = os == "android"
+skip-if = os == "android" # Disabled due to crashes (see bug 1331366)
 tags = addons
 [test_ChildScalars.js]
 skip-if = os == "android" # Disabled due to crashes (see bug 1331366)
 [test_TelemetryReportingPolicy.js]
 tags = addons
 [test_TelemetryScalars.js]
 [test_TelemetryTimestamps.js]
 skip-if = toolkit == 'android'
 [test_TelemetryCaptureStack.js]
 [test_TelemetryEvents.js]
+[test_ChildEvents.js]
+skip-if = os == "android" # Disabled due to crashes (see bug 1331366)
 [test_TelemetryModules.js]
 [test_PingSender.js]
 skip-if = (os == "android") || (os == "linux" && bits == 32)