Bug 1553254 - Part 1: Allow low-overhead selective collection of internal performance probes through ChromeUtils. r=brennie,nika
authorBas Schouten <bschouten@mozilla.com>
Tue, 21 May 2019 21:01:31 +0200
changeset 479010 49028e60c0d4ce7540dcd33da1a0dc47f1e68aa5
parent 479009 14f3762c1f700dc957b4af6dc2b0740364d8d0fd
child 479011 f0310aa07b8f479e9f444a64bedd3d2244b6dd32
push id36156
push userapavel@mozilla.com
push dateSat, 15 Jun 2019 09:57:03 +0000
treeherdermozilla-central@52f3d1a5b791 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbrennie, nika
bugs1553254
milestone69.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1553254 - Part 1: Allow low-overhead selective collection of internal performance probes through ChromeUtils. r=brennie,nika Differential Revision: https://phabricator.services.mozilla.com/D32039
dom/base/ChromeUtils.cpp
dom/base/ChromeUtils.h
dom/chrome-webidl/ChromeUtils.webidl
dom/ipc/ContentChild.cpp
dom/ipc/ContentChild.h
dom/ipc/PContent.ipdl
gfx/ipc/GPUParent.cpp
gfx/ipc/GPUParent.h
gfx/ipc/PGPU.ipdl
toolkit/toolkit.mozbuild
tools/moz.build
tools/performance/PerfStats.cpp
tools/performance/PerfStats.h
tools/performance/moz.build
--- a/dom/base/ChromeUtils.cpp
+++ b/dom/base/ChromeUtils.cpp
@@ -10,16 +10,17 @@
 #include "js/SavedFrameAPI.h"
 #include "jsfriendapi.h"
 #include "WrapperFactory.h"
 
 #include "mozilla/Base64.h"
 #include "mozilla/BasePrincipal.h"
 #include "mozilla/CycleCollectedJSRuntime.h"
 #include "mozilla/PerformanceMetricsCollector.h"
+#include "mozilla/PerfStats.h"
 #include "mozilla/Preferences.h"
 #include "mozilla/ProcInfo.h"
 #include "mozilla/RDDProcessManager.h"
 #include "mozilla/ResultExtensions.h"
 #include "mozilla/TimeStamp.h"
 #include "mozilla/dom/BrowsingContext.h"
 #include "mozilla/dom/ContentParent.h"
 #include "mozilla/dom/IdleDeadline.h"
@@ -860,16 +861,45 @@ already_AddRefed<Promise> ChromeUtils::R
         domPromise->MaybeResolve(std::move(aResults));
       },
       [domPromise](const nsresult& aRv) { domPromise->MaybeReject(aRv); });
 
   // sending back the promise instance
   return domPromise.forget();
 }
 
+void ChromeUtils::SetPerfStatsCollectionMask(GlobalObject& aGlobal,
+                                             uint64_t aMask) {
+  PerfStats::SetCollectionMask(static_cast<PerfStats::MetricMask>(aMask));
+}
+
+already_AddRefed<Promise> ChromeUtils::CollectPerfStats(GlobalObject& aGlobal,
+                                                        ErrorResult& aRv) {
+  // Creating a JS promise
+  nsCOMPtr<nsIGlobalObject> global = do_QueryInterface(aGlobal.GetAsSupports());
+  MOZ_ASSERT(global);
+
+  RefPtr<Promise> promise = Promise::Create(global, aRv);
+  if (aRv.Failed()) {
+    return nullptr;
+  }
+
+  RefPtr<PerfStats::PerfStatsPromise> extPromise =
+      PerfStats::CollectPerfStatsJSON();
+
+  extPromise->Then(
+      GetCurrentThreadSerialEventTarget(), __func__,
+      [promise](const nsCString& aResult) {
+        promise->MaybeResolve(NS_ConvertUTF8toUTF16(aResult));
+      },
+      [promise](bool aValue) { promise->MaybeReject(NS_ERROR_FAILURE); });
+
+  return promise.forget();
+}
+
 constexpr auto kSkipSelfHosted = JS::SavedFrameSelfHosted::Exclude;
 
 /* static */
 void ChromeUtils::GetCallerLocation(const GlobalObject& aGlobal,
                                     nsIPrincipal* aPrincipal,
                                     JS::MutableHandle<JSObject*> aRetval) {
   JSContext* cx = aGlobal.Context();
 
--- a/dom/base/ChromeUtils.h
+++ b/dom/base/ChromeUtils.h
@@ -135,16 +135,21 @@ class ChromeUtils {
                                   JS::MutableHandleValue aRetval,
                                   ErrorResult& aRv);
 
   static void ClearRecentJSDevError(GlobalObject& aGlobal);
 
   static already_AddRefed<Promise> RequestPerformanceMetrics(
       GlobalObject& aGlobal, ErrorResult& aRv);
 
+  static void SetPerfStatsCollectionMask(GlobalObject& aGlobal, uint64_t aMask);
+
+  static already_AddRefed<Promise> CollectPerfStats(GlobalObject& aGlobal,
+                                                    ErrorResult& aRv);
+
   static already_AddRefed<Promise> RequestProcInfo(GlobalObject& aGlobal,
                                                    ErrorResult& aRv);
 
   static void Import(const GlobalObject& aGlobal, const nsAString& aResourceURI,
                      const Optional<JS::Handle<JSObject*>>& aTargetObj,
                      JS::MutableHandle<JSObject*> aRetval, ErrorResult& aRv);
 
   static void DefineModuleGetter(const GlobalObject& global,
--- a/dom/chrome-webidl/ChromeUtils.webidl
+++ b/dom/chrome-webidl/ChromeUtils.webidl
@@ -358,16 +358,33 @@ partial namespace ChromeUtils {
 
   /**
    * Request performance metrics to the current process & all content processes.
    */
   [Throws]
   Promise<sequence<PerformanceInfoDictionary>> requestPerformanceMetrics();
 
   /**
+   * Set the collection of specific detailed performance timing information.
+   * Selecting 0 for the mask will end existing collection. All metrics that
+   * are chosen will be cleared after updating the mask.
+   *
+   * @param aCollectionMask A bitmask where each bit corresponds to a metric
+   *        to be collected as listed in PerfStats::Metric.
+   */
+  void setPerfStatsCollectionMask(unsigned long long aCollectionMask);
+
+  /**
+   * Collect results of detailed performance timing information.
+   * The output is a JSON string containing performance timings.
+   */
+  [Throws]
+  Promise<DOMString> collectPerfStats();
+
+  /**
   * Returns a Promise containing a sequence of I/O activities
   */
   [Throws]
   Promise<sequence<IOActivityDataDictionary>> requestIOActivity();
 
   /**
   * Returns a Promise containing all processes info
   */
--- a/dom/ipc/ContentChild.cpp
+++ b/dom/ipc/ContentChild.cpp
@@ -15,16 +15,17 @@
 #include "BrowserChild.h"
 #include "HandlerServiceChild.h"
 
 #include "mozilla/Attributes.h"
 #include "mozilla/BackgroundHangMonitor.h"
 #include "mozilla/LookAndFeel.h"
 #include "mozilla/MemoryTelemetry.h"
 #include "mozilla/NullPrincipal.h"
+#include "mozilla/PerfStats.h"
 #include "mozilla/Preferences.h"
 #include "mozilla/ProcessHangMonitorIPC.h"
 #include "mozilla/RemoteDecoderManagerChild.h"
 #include "mozilla/Unused.h"
 #include "mozilla/StaticPrefs.h"
 #include "mozilla/TelemetryIPC.h"
 #include "mozilla/RemoteDecoderManagerChild.h"
 #include "mozilla/devtools/HeapSnapshotTempFileHelperChild.h"
@@ -2428,16 +2429,28 @@ mozilla::ipc::IPCResult ContentChild::Re
   return IPC_OK();
 }
 
 mozilla::ipc::IPCResult ContentChild::RecvVarUpdate(const GfxVarUpdate& aVar) {
   gfx::gfxVars::ApplyUpdate(aVar);
   return IPC_OK();
 }
 
+mozilla::ipc::IPCResult ContentChild::RecvUpdatePerfStatsCollectionMask(
+    const uint64_t& aMask) {
+  PerfStats::SetCollectionMask(static_cast<PerfStats::MetricMask>(aMask));
+  return IPC_OK();
+}
+
+mozilla::ipc::IPCResult ContentChild::RecvCollectPerfStatsJSON(
+    CollectPerfStatsJSONResolver&& aResolver) {
+  aResolver(PerfStats::CollectLocalPerfStatsJSON());
+  return IPC_OK();
+}
+
 mozilla::ipc::IPCResult ContentChild::RecvDataStoragePut(
     const nsString& aFilename, const DataStorageItem& aItem) {
   RefPtr<DataStorage> storage = DataStorage::GetFromRawFileName(aFilename);
   if (storage) {
     storage->Put(aItem.key(), aItem.value(), aItem.type());
   }
   return IPC_OK();
 }
--- a/dom/ipc/ContentChild.h
+++ b/dom/ipc/ContentChild.h
@@ -344,16 +344,22 @@ class ContentChild final : public PConte
 
   // auto remove when alertfinished is received.
   nsresult AddRemoteAlertObserver(const nsString& aData,
                                   nsIObserver* aObserver);
 
   mozilla::ipc::IPCResult RecvPreferenceUpdate(const Pref& aPref);
   mozilla::ipc::IPCResult RecvVarUpdate(const GfxVarUpdate& pref);
 
+  mozilla::ipc::IPCResult RecvUpdatePerfStatsCollectionMask(
+      const uint64_t& aMask);
+
+  mozilla::ipc::IPCResult RecvCollectPerfStatsJSON(
+      CollectPerfStatsJSONResolver&& aResolver);
+
   mozilla::ipc::IPCResult RecvDataStoragePut(const nsString& aFilename,
                                              const DataStorageItem& aItem);
 
   mozilla::ipc::IPCResult RecvDataStorageRemove(const nsString& aFilename,
                                                 const nsCString& aKey,
                                                 const DataStorageType& aType);
 
   mozilla::ipc::IPCResult RecvDataStorageClear(const nsString& aFilename);
--- a/dom/ipc/PContent.ipdl
+++ b/dom/ipc/PContent.ipdl
@@ -477,16 +477,19 @@ child:
     async SetConnectivity(bool connectivity);
     async SetCaptivePortalState(int32_t aState);
 
     async NotifyVisited(URIParams[] uri);
 
     async PreferenceUpdate(Pref pref);
     async VarUpdate(GfxVarUpdate var);
 
+    async UpdatePerfStatsCollectionMask(uint64_t aMask);
+    async CollectPerfStatsJSON() returns (nsCString aStats);
+
     async DataStoragePut(nsString aFilename, DataStorageItem aItem);
     async DataStorageRemove(nsString aFilename, nsCString aKey, DataStorageType aType);
     async DataStorageClear(nsString aFilename);
 
     async NotifyAlertsObserver(nsCString topic, nsString data);
 
     async GeolocationUpdate(nsIDOMGeoPosition aPosition);
 
--- a/gfx/ipc/GPUParent.cpp
+++ b/gfx/ipc/GPUParent.cpp
@@ -9,16 +9,17 @@
 #include "GPUParent.h"
 #include "gfxConfig.h"
 #include "gfxCrashReporterUtils.h"
 #include "gfxPlatform.h"
 #include "GLContextProvider.h"
 #include "GPUProcessHost.h"
 #include "GPUProcessManager.h"
 #include "mozilla/Assertions.h"
+#include "mozilla/PerfStats.h"
 #include "mozilla/StaticPrefs.h"
 #include "mozilla/Telemetry.h"
 #include "mozilla/TimeStamp.h"
 #include "mozilla/RemoteDecoderManagerChild.h"
 #include "mozilla/RemoteDecoderManagerParent.h"
 #include "mozilla/dom/MemoryReportRequest.h"
 #include "mozilla/gfx/2D.h"
 #include "mozilla/gfx/gfxVars.h"
@@ -478,16 +479,28 @@ mozilla::ipc::IPCResult GPUParent::RecvR
 
 mozilla::ipc::IPCResult GPUParent::RecvShutdownVR() {
   if (StaticPrefs::VRProcessEnabled()) {
     VRGPUChild::Shutdown();
   }
   return IPC_OK();
 }
 
+mozilla::ipc::IPCResult GPUParent::RecvUpdatePerfStatsCollectionMask(
+    const uint64_t& aMask) {
+  PerfStats::SetCollectionMask(static_cast<PerfStats::MetricMask>(aMask));
+  return IPC_OK();
+}
+
+mozilla::ipc::IPCResult GPUParent::RecvCollectPerfStatsJSON(
+    CollectPerfStatsJSONResolver&& aResolver) {
+  aResolver(PerfStats::CollectLocalPerfStatsJSON());
+  return IPC_OK();
+}
+
 void GPUParent::ActorDestroy(ActorDestroyReason aWhy) {
   if (AbnormalShutdown == aWhy) {
     NS_WARNING("Shutting down GPU process early due to a crash!");
     ProcessChild::QuickExit();
   }
 
 #ifdef XP_WIN
   wmf::MFShutdown();
--- a/gfx/ipc/GPUParent.h
+++ b/gfx/ipc/GPUParent.h
@@ -72,16 +72,21 @@ class GPUParent final : public PGPUParen
       const LayerTreeIdMapping& aMapping);
   mozilla::ipc::IPCResult RecvNotifyGpuObservers(const nsCString& aTopic);
   mozilla::ipc::IPCResult RecvRequestMemoryReport(
       const uint32_t& generation, const bool& anonymize,
       const bool& minimizeMemoryUsage,
       const Maybe<ipc::FileDescriptor>& DMDFile);
   mozilla::ipc::IPCResult RecvShutdownVR();
 
+  mozilla::ipc::IPCResult RecvUpdatePerfStatsCollectionMask(
+      const uint64_t& aMask);
+  mozilla::ipc::IPCResult RecvCollectPerfStatsJSON(
+      CollectPerfStatsJSONResolver&& aResolver);
+
   void ActorDestroy(ActorDestroyReason aWhy) override;
 
  private:
   const TimeStamp mLaunchTime;
   RefPtr<VsyncBridgeParent> mVsyncBridge;
 #ifdef MOZ_GECKO_PROFILER
   RefPtr<ChildProfilerController> mProfilerController;
 #endif
--- a/gfx/ipc/PGPU.ipdl
+++ b/gfx/ipc/PGPU.ipdl
@@ -90,16 +90,20 @@ parent:
   async NotifyGpuObservers(nsCString aTopic);
 
   async RequestMemoryReport(uint32_t generation,
                             bool anonymize,
                             bool minimizeMemoryUsage,
                             FileDescriptor? DMDFile);
   async ShutdownVR();
 
+  // Functions supporting PerfStats data collection.
+  async UpdatePerfStatsCollectionMask(uint64_t aMask);
+  async CollectPerfStatsJSON() returns (nsCString aStats);
+
 child:
   // Sent when the GPU process has initialized devices. This occurs once, after
   // Init().
   async InitComplete(GPUDeviceData data);
 
   // Sent when APZ detects checkerboarding and apz checkerboard reporting is enabled.
   async ReportCheckerboard(uint32_t severity, nsCString log);
 
--- a/toolkit/toolkit.mozbuild
+++ b/toolkit/toolkit.mozbuild
@@ -126,16 +126,17 @@ else:
 # toolkit
 
 # This must precede xpfe.
 if CONFIG['MOZ_JPROF']:
     DIRS += ['/tools/jprof']
 
 DIRS += [
     '/tools/code-coverage',
+    '/tools/performance',
     '/tools/power',
     '/tools/profiler',
 ]
 
 if CONFIG['MOZ_SPELLCHECK']:
     DIRS += ['/extensions/spellcheck']
 
 DIRS += [
--- a/tools/moz.build
+++ b/tools/moz.build
@@ -24,16 +24,19 @@ with Files("docs/**"):
     SCHEDULES.exclusive = ['docs']
 
 with Files("lint/**"):
     BUG_COMPONENT = ("Firefox Build System", "Lint and Formatting")
 
 with Files("profiler/**"):
     BUG_COMPONENT = ("Core", "Gecko Profiler")
 
+with Files("performance/**"):
+    BUG_COMPONENT = ("Core", "Gecko Profiler")
+
 with Files("quitter/**"):
     BUG_COMPONENT = ("Testing", "General")
 
 with Files("rb/**"):
     BUG_COMPONENT = ("Core", "XPCOM")
 
 with Files("rewriting/**"):
     BUG_COMPONENT = ("Firefox Build System", "Source Code Analysis")
new file mode 100644
--- /dev/null
+++ b/tools/performance/PerfStats.cpp
@@ -0,0 +1,260 @@
+/* -*- Mode: C++; tab-width: 20; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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 "PerfStats.h"
+#include "nsAppRunner.h"
+#include "mozilla/dom/BrowserParent.h"
+#include "mozilla/dom/CanonicalBrowsingContext.h"
+#include "mozilla/dom/ContentParent.h"
+#include "mozilla/dom/ContentProcessManager.h"
+#include "mozilla/dom/WindowGlobalParent.h"
+#include "mozilla/gfx/GPUChild.h"
+#include "mozilla/gfx/GPUProcessManager.h"
+#include "mozilla/JSONWriter.h"
+
+using namespace mozilla::dom;
+using namespace mozilla::gfx;
+
+namespace mozilla {
+
+static const char* const sMetricNames[] = {"DisplayList Building",
+                                           "Rasterizing",
+                                           "LayerBuilding",
+                                           "Layer Transactions",
+                                           "Compositing",
+                                           "Reflowing",
+                                           "Styling"};
+
+PerfStats::MetricMask PerfStats::sCollectionMask = 0;
+StaticMutex PerfStats::sMutex;
+StaticAutoPtr<PerfStats> PerfStats::sSingleton;
+
+void PerfStats::SetCollectionMask(MetricMask aMask) {
+  sCollectionMask = aMask;
+  for (uint64_t i = 0; i < static_cast<uint64_t>(Metric::Max); i++) {
+    if (!(sCollectionMask & 1 << i)) {
+      continue;
+    }
+
+    GetSingleton()->mRecordedTimes[i] = 0;
+  }
+
+  if (!XRE_IsParentProcess()) {
+    return;
+  }
+
+  GPUProcessManager* gpuManager = GPUProcessManager::Get();
+  GPUChild* gpuChild = nullptr;
+
+  if (gpuManager) {
+    gpuChild = gpuManager->GetGPUChild();
+    if (gpuChild) {
+      gpuChild->SendUpdatePerfStatsCollectionMask(aMask);
+    }
+  }
+
+  nsTArray<ContentParent*> contentParents;
+  ContentParent::GetAll(contentParents);
+
+  for (ContentParent* parent : contentParents) {
+    Unused << parent->SendUpdatePerfStatsCollectionMask(aMask);
+  }
+}
+
+PerfStats* PerfStats::GetSingleton() {
+  if (!sSingleton) {
+    sSingleton = new PerfStats;
+  }
+
+  return sSingleton.get();
+}
+
+void PerfStats::RecordMeasurementStartInternal(Metric aMetric) {
+  StaticMutexAutoLock lock(sMutex);
+
+  GetSingleton()->mRecordedStarts[static_cast<size_t>(aMetric)] =
+      TimeStamp::Now();
+}
+
+void PerfStats::RecordMeasurementEndInternal(Metric aMetric) {
+  StaticMutexAutoLock lock(sMutex);
+
+  MOZ_ASSERT(sSingleton);
+
+  sSingleton->mRecordedTimes[static_cast<size_t>(aMetric)] +=
+      (TimeStamp::Now() -
+       sSingleton->mRecordedStarts[static_cast<size_t>(aMetric)])
+          .ToMilliseconds();
+}
+
+struct StringWriteFunc : public JSONWriteFunc {
+  nsCString& mString;
+
+  explicit StringWriteFunc(nsCString& aString) : mString(aString) {}
+  virtual void Write(const char* aStr) override { mString.Append(aStr); }
+};
+
+void AppendJSONStringAsProperty(nsCString& aDest, const char* aPropertyName,
+                                const nsCString& aJSON) {
+  // We need to manually append into the string here, since JSONWriter has no
+  // way to allow us to write an existing JSON object into a property.
+  aDest.Append(",\n\"");
+  aDest.Append(aPropertyName);
+  aDest.Append("\": ");
+  aDest.Append(aJSON);
+}
+
+struct PerfStatsCollector {
+  PerfStatsCollector() : writer(MakeUnique<StringWriteFunc>(string)) {}
+
+  void AppendPerfStats(const nsCString& aString, ContentParent* aParent) {
+    writer.StartObjectElement();
+    writer.StringProperty("type", "content");
+    writer.IntProperty("id", aParent->ChildID());
+    const ManagedContainer<PBrowserParent>& browsers =
+        aParent->ManagedPBrowserParent();
+
+    writer.StartArrayProperty("urls");
+    for (auto iter = browsers.ConstIter(); !iter.Done(); iter.Next()) {
+      RefPtr<BrowserParent> parent =
+          BrowserParent::GetFrom(iter.Get()->GetKey());
+
+      CanonicalBrowsingContext* ctx = parent->GetBrowsingContext();
+      if (!ctx) {
+        continue;
+      }
+
+      WindowGlobalParent* windowGlobal = ctx->GetCurrentWindowGlobal();
+      if (!windowGlobal) {
+        continue;
+      }
+
+      RefPtr<nsIURI> uri = windowGlobal->GetDocumentURI();
+      if (!uri) {
+        continue;
+      }
+
+      nsAutoCString url;
+      uri->GetSpec(url);
+
+      writer.StringElement(url.BeginReading());
+    }
+    writer.EndArray();
+    AppendJSONStringAsProperty(string, "perfstats", aString);
+    writer.EndObject();
+  }
+
+  void AppendPerfStats(const nsCString& aString, GPUChild* aChild) {
+    writer.StartObjectElement();
+    writer.StringProperty("type", "gpu");
+    writer.IntProperty("id", aChild->Id());
+    AppendJSONStringAsProperty(string, "perfstats", aString);
+    writer.EndObject();
+  }
+
+  ~PerfStatsCollector() {
+    writer.EndArray();
+    writer.End();
+    promise.Resolve(string, __func__);
+  }
+  nsCString string;
+  JSONWriter writer;
+  MozPromiseHolder<PerfStats::PerfStatsPromise> promise;
+};
+
+auto PerfStats::CollectPerfStatsJSONInternal() -> RefPtr<PerfStatsPromise> {
+  if (!PerfStats::sCollectionMask) {
+    return PerfStatsPromise::CreateAndReject(false, __func__);
+  }
+
+  if (!XRE_IsParentProcess()) {
+    return PerfStatsPromise::CreateAndResolve(
+        CollectLocalPerfStatsJSONInternal(), __func__);
+  }
+
+  std::shared_ptr<PerfStatsCollector> collector =
+      std::make_shared<PerfStatsCollector>();
+
+  JSONWriter& w = collector->writer;
+
+  w.Start();
+  {
+    w.StartArrayProperty("processes");
+    {
+      w.StartObjectElement();
+      {
+        w.StringProperty("type", "parent");
+        AppendJSONStringAsProperty(collector->string, "perfstats",
+                                   CollectLocalPerfStatsJSONInternal());
+      }
+      w.EndObject();
+
+      GPUProcessManager* gpuManager = GPUProcessManager::Get();
+      GPUChild* gpuChild = nullptr;
+
+      if (gpuManager) {
+        gpuChild = gpuManager->GetGPUChild();
+      }
+      nsTArray<ContentParent*> contentParents;
+      ContentParent::GetAll(contentParents);
+
+      if (gpuChild) {
+        gpuChild->SendCollectPerfStatsJSON(
+            [collector, gpuChild](const nsCString& aString) {
+              collector->AppendPerfStats(aString, gpuChild);
+            },
+            // The only feasible errors here are if something goes wrong in the
+            // the bridge, we choose to ignore those.
+            [](mozilla::ipc::ResponseRejectReason) {});
+      }
+      for (ContentParent* parent : contentParents) {
+        RefPtr<ContentParent> parentRef = parent;
+        parent->SendCollectPerfStatsJSON(
+            [collector, parentRef](const nsCString& aString) {
+              collector->AppendPerfStats(aString, parentRef.get());
+            },
+            // The only feasible errors here are if something goes wrong in the
+            // the bridge, we choose to ignore those.
+            [](mozilla::ipc::ResponseRejectReason) {});
+      }
+    }
+  }
+
+  return collector->promise.Ensure(__func__);
+}
+
+nsCString PerfStats::CollectLocalPerfStatsJSONInternal() {
+  StaticMutexAutoLock lock(PerfStats::sMutex);
+
+  nsCString jsonString;
+
+  JSONWriter w(MakeUnique<StringWriteFunc>(jsonString));
+  w.Start();
+  {
+    w.StartArrayProperty("metrics");
+    {
+      for (uint64_t i = 0; i < static_cast<uint64_t>(Metric::Max); i++) {
+        if (!(sCollectionMask & (1 << i))) {
+          continue;
+        }
+
+        w.StartObjectElement();
+        {
+          w.IntProperty("id", i);
+          w.StringProperty("metric", sMetricNames[i]);
+          w.DoubleProperty("time", mRecordedTimes[i]);
+        }
+        w.EndObject();
+      }
+    }
+    w.EndArray();
+  }
+  w.End();
+
+  return jsonString;
+}
+
+}  // namespace mozilla
new file mode 100644
--- /dev/null
+++ b/tools/performance/PerfStats.h
@@ -0,0 +1,91 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* 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/. */
+
+#ifndef PerfStats_h
+#define PerfStats_h
+
+#include "mozilla/TimeStamp.h"
+#include "mozilla/StaticMutex.h"
+#include "mozilla/StaticPtr.h"
+#include "mozilla/MozPromise.h"
+#include <memory>
+#include <string>
+#include <limits>
+
+namespace mozilla {
+
+class PerfStats {
+ public:
+  typedef MozPromise<nsCString, bool, true> PerfStatsPromise;
+
+  enum class Metric : uint32_t {
+    DisplayListBuilding = 0,
+    Rasterizing,
+    LayerBuilding,
+    LayerTransactions,
+    Compositing,
+    Reflowing,
+    Styling,
+    Max
+  };
+
+  // MetricMask is a bitmask based on 'Metric', i.e. Metric::LayerBuilding (2)
+  // is synonymous to 1 << 2 in MetricMask.
+  using MetricMask = uint64_t;
+
+  static void RecordMeasurementStart(Metric aMetric) {
+    if (!(sCollectionMask & (1 << static_cast<uint64_t>(aMetric)))) {
+      return;
+    }
+    RecordMeasurementStartInternal(aMetric);
+  }
+
+  static void RecordMeasurementEnd(Metric aMetric) {
+    if (!(sCollectionMask & (1 << static_cast<uint64_t>(aMetric)))) {
+      return;
+    }
+    RecordMeasurementEndInternal(aMetric);
+  }
+
+  template <Metric N>
+  class AutoMetricRecording {
+   public:
+    AutoMetricRecording() { PerfStats::RecordMeasurementStart(N); }
+    ~AutoMetricRecording() { PerfStats::RecordMeasurementEnd(N); }
+  };
+
+  static void SetCollectionMask(MetricMask aMask);
+
+  static RefPtr<PerfStatsPromise> CollectPerfStatsJSON() {
+    return GetSingleton()->CollectPerfStatsJSONInternal();
+  }
+
+  static nsCString CollectLocalPerfStatsJSON() {
+    return GetSingleton()->CollectLocalPerfStatsJSONInternal();
+  }
+
+ private:
+  static PerfStats* GetSingleton();
+  static void RecordMeasurementStartInternal(Metric aMetric);
+  static void RecordMeasurementEndInternal(Metric aMetric);
+
+  RefPtr<PerfStatsPromise> CollectPerfStatsJSONInternal();
+  nsCString CollectLocalPerfStatsJSONInternal();
+
+  static MetricMask sCollectionMask;
+  static StaticMutex sMutex;
+  static StaticAutoPtr<PerfStats> sSingleton;
+  TimeStamp mRecordedStarts[static_cast<size_t>(Metric::Max)];
+  double mRecordedTimes[static_cast<size_t>(Metric::Max)];
+};
+
+static_assert(1 << (static_cast<uint64_t>(PerfStats::Metric::Max) - 1) <=
+                  std::numeric_limits<PerfStats::MetricMask>::max(),
+              "More metrics than can fit into sCollectionMask bitmask");
+
+}  // namespace mozilla
+
+#endif  // PerfStats_h
new file mode 100644
--- /dev/null
+++ b/tools/performance/moz.build
@@ -0,0 +1,17 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+UNIFIED_SOURCES += [
+    'PerfStats.cpp',
+]
+
+EXPORTS.mozilla += [
+    'PerfStats.h',
+]
+
+include('/ipc/chromium/chromium-config.mozbuild')
+
+FINAL_LIBRARY = 'xul'