Merge mozilla-central to mozilla-inbound. a=merge
authorDaniel Varga <dvarga@mozilla.com>
Sat, 08 Dec 2018 11:50:00 +0200
changeset 508908 4c4645fdcf1cd1a18e03a0689fcc5528e0836a01
parent 508907 a72dafbb2e8089c38cf155f846e2256b90c1aa9e (current diff)
parent 508871 336f58aeb663c01ede2a646d51d5015bf741538d (diff)
child 508909 98f8e4e44c103044d3a6d9a27bd2e8586fe4f05e
push id1905
push userffxbld-merge
push dateMon, 21 Jan 2019 12:33:13 +0000
treeherdermozilla-release@c2fca1944d8c [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone65.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
Merge mozilla-central to mozilla-inbound. a=merge
gfx/layers/ipc/LayersMessageUtils.h
testing/web-platform/meta/streams/readable-streams/bad-underlying-sources.dedicatedworker.html.ini
testing/web-platform/meta/streams/readable-streams/bad-underlying-sources.html.ini
testing/web-platform/meta/streams/readable-streams/bad-underlying-sources.serviceworker.https.html.ini
testing/web-platform/meta/streams/readable-streams/bad-underlying-sources.sharedworker.html.ini
testing/web-platform/meta/streams/readable-streams/garbage-collection.dedicatedworker.html.ini
testing/web-platform/meta/streams/readable-streams/garbage-collection.html.ini
testing/web-platform/meta/streams/readable-streams/garbage-collection.serviceworker.https.html.ini
testing/web-platform/meta/streams/readable-streams/garbage-collection.sharedworker.html.ini
--- a/browser/components/urlbar/UrlbarView.jsm
+++ b/browser/components/urlbar/UrlbarView.jsm
@@ -192,27 +192,30 @@ class UrlbarView {
     }
   }
 
   _addRow(resultIndex) {
     let result = this._queryContext.results[resultIndex];
     let item = this._createElement("div");
     item.className = "urlbarView-row";
     item.setAttribute("resultIndex", resultIndex);
+
     if (result.type == UrlbarUtils.MATCH_TYPE.TAB_SWITCH) {
-      item.setAttribute("action", "switch-to-tab");
+      item.setAttribute("type", "switchtab");
+    } else if (result.source == UrlbarUtils.MATCH_SOURCE.BOOKMARKS) {
+      item.setAttribute("type", "bookmark");
     }
 
     let content = this._createElement("span");
     content.className = "urlbarView-row-inner";
     item.appendChild(content);
 
-    let actionIcon = this._createElement("span");
-    actionIcon.className = "urlbarView-action-icon";
-    content.appendChild(actionIcon);
+    let typeIcon = this._createElement("span");
+    typeIcon.className = "urlbarView-type-icon";
+    content.appendChild(typeIcon);
 
     let favicon = this._createElement("img");
     favicon.className = "urlbarView-favicon";
     favicon.src = result.payload.icon || "chrome://mozapps/skin/places/defaultFavicon.svg";
     content.appendChild(favicon);
 
     let title = this._createElement("span");
     title.className = "urlbarView-title";
--- a/browser/themes/shared/urlbar-autocomplete.inc.css
+++ b/browser/themes/shared/urlbar-autocomplete.inc.css
@@ -63,31 +63,35 @@
   background: var(--arrowpanel-dimmed);
 }
 
 .urlbarView-row[selected] {
   background: var(--autocomplete-popup-highlight-background);
   color: var(--autocomplete-popup-highlight-color);
 }
 
-.urlbarView-action-icon,
+.urlbarView-type-icon,
 .urlbarView-favicon {
   display: inline-block;
   vertical-align: middle;
   width: 16px;
   height: 16px;
   margin-inline-end: @urlbarViewIconMarginEnd@;
   background-repeat: no-repeat;
   background-size: contain;
   -moz-context-properties: fill, fill-opacity;
   fill: currentColor;
   fill-opacity: 0.6;
 }
 
-.urlbarView-row[action=switch-to-tab] > .urlbarView-row-inner > .urlbarView-action-icon {
+.urlbarView-row[type=bookmark] > .urlbarView-row-inner > .urlbarView-type-icon {
+  background-image: url(chrome://browser/skin/bookmark.svg);
+}
+
+.urlbarView-row[type=switchtab] > .urlbarView-row-inner > .urlbarView-type-icon {
   background-image: url(chrome://browser/skin/tab.svg);
 }
 
 .urlbarView-title {
   font-size: 1.05em;
 }
 
 .urlbarView-title::after {
--- a/gfx/ipc/CompositorWidgetVsyncObserver.cpp
+++ b/gfx/ipc/CompositorWidgetVsyncObserver.cpp
@@ -13,19 +13,19 @@ namespace widget {
 CompositorWidgetVsyncObserver::CompositorWidgetVsyncObserver(
     RefPtr<VsyncBridgeChild> aVsyncBridge,
     const layers::LayersId& aRootLayerTreeId)
     : mVsyncBridge(aVsyncBridge), mRootLayerTreeId(aRootLayerTreeId) {
   MOZ_ASSERT(XRE_IsParentProcess());
   MOZ_ASSERT(NS_IsMainThread());
 }
 
-bool CompositorWidgetVsyncObserver::NotifyVsync(TimeStamp aTimeStamp) {
+bool CompositorWidgetVsyncObserver::NotifyVsync(const VsyncEvent& aVsync) {
   // Vsync notifications should only arrive on the vsync thread.
   MOZ_ASSERT(XRE_IsParentProcess());
   MOZ_ASSERT(!NS_IsMainThread());
 
-  mVsyncBridge->NotifyVsync(aTimeStamp, mRootLayerTreeId);
+  mVsyncBridge->NotifyVsync(aVsync, mRootLayerTreeId);
   return true;
 }
 
 }  // namespace widget
 }  // namespace mozilla
--- a/gfx/ipc/CompositorWidgetVsyncObserver.h
+++ b/gfx/ipc/CompositorWidgetVsyncObserver.h
@@ -19,17 +19,17 @@ namespace widget {
 
 class CompositorWidgetVsyncObserver : public VsyncObserver {
   typedef gfx::VsyncBridgeChild VsyncBridgeChild;
 
  public:
   CompositorWidgetVsyncObserver(RefPtr<VsyncBridgeChild> aVsyncBridge,
                                 const layers::LayersId& aRootLayerTreeId);
 
-  bool NotifyVsync(TimeStamp aVsyncTimestamp) override;
+  bool NotifyVsync(const VsyncEvent& aVsync) override;
 
  private:
   RefPtr<VsyncBridgeChild> mVsyncBridge;
   layers::LayersId mRootLayerTreeId;
 };
 
 }  // namespace widget
 }  // namespace mozilla
--- a/gfx/ipc/PVsyncBridge.ipdl
+++ b/gfx/ipc/PVsyncBridge.ipdl
@@ -1,23 +1,24 @@
 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 /* 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/. */
 
 using class mozilla::TimeStamp from "mozilla/TimeStamp.h";
 using mozilla::layers::LayersId from "mozilla/layers/LayersTypes.h";
+using mozilla::VsyncEvent from "mozilla/VsyncDispatcher.h";
 
 namespace mozilla {
 namespace gfx {
 
 // This protocol only serves one purpose: deliver vsync notifications from a
 // dedicated thread in the UI process to the compositor thread in the
 // compositor process. The child side exists in the UI process, and the
 // parent side in the GPU process.
 sync protocol PVsyncBridge
 {
 parent:
-  async NotifyVsync(TimeStamp vsyncTimeStamp, LayersId layersId);
+  async NotifyVsync(VsyncEvent vsync, LayersId layersId);
 };
 
 } // namespace gfx
 } // namespace mozilla
--- a/gfx/ipc/VsyncBridgeChild.cpp
+++ b/gfx/ipc/VsyncBridgeChild.cpp
@@ -41,57 +41,56 @@ void VsyncBridgeChild::Open(Endpoint<PVs
   mLoop = MessageLoop::current();
 
   // Last reference is freed in DeallocPVsyncBridgeChild.
   AddRef();
 }
 
 class NotifyVsyncTask : public Runnable {
  public:
-  NotifyVsyncTask(RefPtr<VsyncBridgeChild> aVsyncBridge, TimeStamp aTimeStamp,
-                  const layers::LayersId& aLayersId)
+  NotifyVsyncTask(RefPtr<VsyncBridgeChild> aVsyncBridge,
+                  const VsyncEvent& aVsync, const layers::LayersId& aLayersId)
       : Runnable("gfx::NotifyVsyncTask"),
         mVsyncBridge(aVsyncBridge),
-        mTimeStamp(aTimeStamp),
+        mVsync(aVsync),
         mLayersId(aLayersId) {}
 
   NS_IMETHOD Run() override {
-    mVsyncBridge->NotifyVsyncImpl(mTimeStamp, mLayersId);
+    mVsyncBridge->NotifyVsyncImpl(mVsync, mLayersId);
     return NS_OK;
   }
 
  private:
   RefPtr<VsyncBridgeChild> mVsyncBridge;
-  TimeStamp mTimeStamp;
+  VsyncEvent mVsync;
   layers::LayersId mLayersId;
 };
 
 bool VsyncBridgeChild::IsOnVsyncIOThread() const {
   return MessageLoop::current() == mLoop;
 }
 
-void VsyncBridgeChild::NotifyVsync(TimeStamp aTimeStamp,
+void VsyncBridgeChild::NotifyVsync(const VsyncEvent& aVsync,
                                    const layers::LayersId& aLayersId) {
   // This should be on the Vsync thread (not the Vsync I/O thread).
   MOZ_ASSERT(!IsOnVsyncIOThread());
 
-  RefPtr<NotifyVsyncTask> task =
-      new NotifyVsyncTask(this, aTimeStamp, aLayersId);
+  RefPtr<NotifyVsyncTask> task = new NotifyVsyncTask(this, aVsync, aLayersId);
   mLoop->PostTask(task.forget());
 }
 
-void VsyncBridgeChild::NotifyVsyncImpl(TimeStamp aTimeStamp,
+void VsyncBridgeChild::NotifyVsyncImpl(const VsyncEvent& aVsync,
                                        const layers::LayersId& aLayersId) {
   // This should be on the Vsync I/O thread.
   MOZ_ASSERT(IsOnVsyncIOThread());
 
   if (!mProcessToken) {
     return;
   }
-  SendNotifyVsync(aTimeStamp, aLayersId);
+  SendNotifyVsync(aVsync, aLayersId);
 }
 
 void VsyncBridgeChild::Close() {
   if (!IsOnVsyncIOThread()) {
     mLoop->PostTask(NewRunnableMethod("gfx::VsyncBridgeChild::Close", this,
                                       &VsyncBridgeChild::Close));
     return;
   }
--- a/gfx/ipc/VsyncBridgeChild.h
+++ b/gfx/ipc/VsyncBridgeChild.h
@@ -25,27 +25,28 @@ class VsyncBridgeChild final : public PV
       Endpoint<PVsyncBridgeChild>&& aEndpoint);
 
   void Close();
 
   void ActorDestroy(ActorDestroyReason aWhy) override;
   void DeallocPVsyncBridgeChild() override;
   void ProcessingError(Result aCode, const char* aReason) override;
 
-  void NotifyVsync(TimeStamp aTimeStamp, const layers::LayersId& aLayersId);
+  void NotifyVsync(const VsyncEvent& aVsync, const layers::LayersId& aLayersId);
 
   virtual void HandleFatalError(const char* aMsg) const override;
 
  private:
   VsyncBridgeChild(RefPtr<VsyncIOThreadHolder>, const uint64_t& aProcessToken);
   ~VsyncBridgeChild();
 
   void Open(Endpoint<PVsyncBridgeChild>&& aEndpoint);
 
-  void NotifyVsyncImpl(TimeStamp aTimeStamp, const layers::LayersId& aLayersId);
+  void NotifyVsyncImpl(const VsyncEvent& aVsync,
+                       const layers::LayersId& aLayersId);
 
   bool IsOnVsyncIOThread() const;
 
  private:
   RefPtr<VsyncIOThreadHolder> mThread;
   MessageLoop* mLoop;
   uint64_t mProcessToken;
 };
--- a/gfx/ipc/VsyncBridgeParent.cpp
+++ b/gfx/ipc/VsyncBridgeParent.cpp
@@ -37,18 +37,18 @@ void VsyncBridgeParent::Open(Endpoint<PV
     // We can't recover from this.
     MOZ_CRASH("Failed to bind VsyncBridgeParent to endpoint");
   }
   AddRef();
   mOpen = true;
 }
 
 mozilla::ipc::IPCResult VsyncBridgeParent::RecvNotifyVsync(
-    const TimeStamp& aTimeStamp, const LayersId& aLayersId) {
-  CompositorBridgeParent::NotifyVsync(aTimeStamp, aLayersId);
+    const VsyncEvent& aVsync, const LayersId& aLayersId) {
+  CompositorBridgeParent::NotifyVsync(aVsync, aLayersId);
   return IPC_OK();
 }
 
 void VsyncBridgeParent::Shutdown() {
   MessageLoop* ccloop = CompositorThreadHolder::Loop();
   if (MessageLoop::current() != ccloop) {
     ccloop->PostTask(NewRunnableMethod("gfx::VsyncBridgeParent::ShutdownImpl",
                                        this, &VsyncBridgeParent::ShutdownImpl));
--- a/gfx/ipc/VsyncBridgeParent.h
+++ b/gfx/ipc/VsyncBridgeParent.h
@@ -18,17 +18,17 @@ namespace gfx {
 
 class VsyncBridgeParent final : public PVsyncBridgeParent {
  public:
   NS_INLINE_DECL_THREADSAFE_REFCOUNTING(VsyncBridgeParent)
 
   static RefPtr<VsyncBridgeParent> Start(
       Endpoint<PVsyncBridgeParent>&& aEndpoint);
 
-  mozilla::ipc::IPCResult RecvNotifyVsync(const TimeStamp& vsyncTimeStamp,
+  mozilla::ipc::IPCResult RecvNotifyVsync(const VsyncEvent& aVsync,
                                           const LayersId& aLayersId) override;
   void ActorDestroy(ActorDestroyReason aWhy) override;
   void DeallocPVsyncBridgeParent() override;
 
   void Shutdown();
 
  private:
   VsyncBridgeParent();
--- a/gfx/layers/LayersTypes.h
+++ b/gfx/layers/LayersTypes.h
@@ -74,49 +74,61 @@ struct LayersId {
   //   std::unordered_map<LayersId, ValueType, LayersId::HashFn> myMap;
   struct HashFn {
     std::size_t operator()(const LayersId& aKey) const {
       return std::hash<uint64_t>{}(aKey.mId);
     }
   };
 };
 
-struct TransactionId {
-  uint64_t mId;
+template <typename T>
+struct BaseTransactionId {
+  uint64_t mId = 0;
 
   bool IsValid() const { return mId != 0; }
 
-  MOZ_MUST_USE TransactionId Next() const { return TransactionId{mId + 1}; }
+  MOZ_MUST_USE BaseTransactionId<T> Next() const {
+    return BaseTransactionId<T>{mId + 1};
+  }
 
-  MOZ_MUST_USE TransactionId Prev() const { return TransactionId{mId - 1}; }
+  MOZ_MUST_USE BaseTransactionId<T> Prev() const {
+    return BaseTransactionId<T>{mId - 1};
+  }
 
-  int64_t operator-(const TransactionId& aOther) const {
+  int64_t operator-(const BaseTransactionId<T>& aOther) const {
     return mId - aOther.mId;
   }
 
   // Allow explicit cast to a uint64_t for now
   explicit operator uint64_t() const { return mId; }
 
-  bool operator<(const TransactionId& aOther) const { return mId < aOther.mId; }
+  bool operator<(const BaseTransactionId<T>& aOther) const {
+    return mId < aOther.mId;
+  }
 
-  bool operator<=(const TransactionId& aOther) const {
+  bool operator<=(const BaseTransactionId<T>& aOther) const {
     return mId <= aOther.mId;
   }
 
-  bool operator>(const TransactionId& aOther) const { return mId > aOther.mId; }
+  bool operator>(const BaseTransactionId<T>& aOther) const {
+    return mId > aOther.mId;
+  }
 
-  bool operator>=(const TransactionId& aOther) const {
+  bool operator>=(const BaseTransactionId<T>& aOther) const {
     return mId >= aOther.mId;
   }
 
-  bool operator==(const TransactionId& aOther) const {
+  bool operator==(const BaseTransactionId<T>& aOther) const {
     return mId == aOther.mId;
   }
 };
 
+class TransactionIdType {};
+typedef BaseTransactionId<TransactionIdType> TransactionId;
+
 struct LayersObserverEpoch {
   uint64_t mId;
 
   MOZ_MUST_USE LayersObserverEpoch Next() const {
     return LayersObserverEpoch{mId + 1};
   }
 
   bool operator<=(const LayersObserverEpoch& aOther) const {
--- a/gfx/layers/TransactionIdAllocator.h
+++ b/gfx/layers/TransactionIdAllocator.h
@@ -5,16 +5,17 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #ifndef GFX_TRANSACTION_ID_ALLOCATOR_H
 #define GFX_TRANSACTION_ID_ALLOCATOR_H
 
 #include "nsISupportsImpl.h"
 #include "mozilla/layers/LayersTypes.h"
 #include "mozilla/TimeStamp.h"
+#include "mozilla/VsyncDispatcher.h"
 
 namespace mozilla {
 namespace layers {
 
 class TransactionIdAllocator {
  protected:
   virtual ~TransactionIdAllocator() {}
 
@@ -75,14 +76,16 @@ class TransactionIdAllocator {
    * completed transactions of previous refresh driver will be ignored.
    */
   virtual void ResetInitialTransactionId(TransactionId aTransactionId) = 0;
 
   /**
    * Get the start time of the current refresh tick.
    */
   virtual mozilla::TimeStamp GetTransactionStart() = 0;
+
+  virtual VsyncId GetVsyncId() = 0;
 };
 
 }  // namespace layers
 }  // namespace mozilla
 
 #endif /* GFX_TRANSACTION_ID_ALLOCATOR_H */
--- a/gfx/layers/client/ClientLayerManager.cpp
+++ b/gfx/layers/client/ClientLayerManager.cpp
@@ -676,27 +676,31 @@ void ClientLayerManager::ForwardTransact
     syncObject->Synchronize();
   }
 
   mPhase = PHASE_FORWARD;
 
   mLatestTransactionId =
       mTransactionIdAllocator->GetTransactionId(!mIsRepeatTransaction);
   TimeStamp refreshStart = mTransactionIdAllocator->GetTransactionStart();
+  if (!refreshStart) {
+    refreshStart = mTransactionStart;
+  }
 
   if (gfxPrefs::AlwaysPaint() && XRE_IsContentProcess()) {
     mForwarder->SendPaintTime(mLatestTransactionId, mLastPaintTime);
   }
 
   // forward this transaction's changeset to our LayerManagerComposite
   bool sent = false;
-  bool ok = mForwarder->EndTransaction(mRegionToClear, mLatestTransactionId,
-                                       aScheduleComposite, mPaintSequenceNumber,
-                                       mIsRepeatTransaction, refreshStart,
-                                       mTransactionStart, mURL, &sent);
+  bool ok = mForwarder->EndTransaction(
+      mRegionToClear, mLatestTransactionId, aScheduleComposite,
+      mPaintSequenceNumber, mIsRepeatTransaction,
+      mTransactionIdAllocator->GetVsyncId(), refreshStart, mTransactionStart,
+      mURL, &sent);
   if (ok) {
     if (sent) {
       mNeedsComposite = false;
     }
   } else if (HasShadowManager()) {
     NS_WARNING("failed to forward Layers transaction");
   }
 
--- a/gfx/layers/ipc/CompositorBridgeParent.cpp
+++ b/gfx/layers/ipc/CompositorBridgeParent.cpp
@@ -666,21 +666,22 @@ void CompositorBridgeParent::PauseCompos
   MonitorAutoLock lock(mPauseCompositionMonitor);
 
   if (!mPaused) {
     mPaused = true;
 
     TimeStamp now = TimeStamp::Now();
     if (mCompositor) {
       mCompositor->Pause();
-      DidComposite(now, now);
+      DidComposite(VsyncId(), now, now);
     } else if (mWrBridge) {
       mWrBridge->Pause();
       NotifyPipelineRendered(mWrBridge->PipelineId(),
-                             mWrBridge->GetCurrentEpoch(), now, now, now);
+                             mWrBridge->GetCurrentEpoch(), VsyncId(), now, now,
+                             now);
     }
   }
 
   // if anyone's waiting to make sure that composition really got paused, tell
   // them
   lock.NotifyAll();
 }
 
@@ -883,28 +884,28 @@ void CompositorBridgeParent::ScheduleCom
       layerCompositor->SetShadowOpacity(layer->GetOpacity());
       layerCompositor->SetShadowOpacitySetByAnimation(false);
     }
     layerCompositor->SetShadowVisibleRegion(layer->GetVisibleRegion());
     layerCompositor->SetShadowClipRect(layer->GetClipRect());
   });
 }
 
-void CompositorBridgeParent::CompositeToTarget(DrawTarget* aTarget,
+void CompositorBridgeParent::CompositeToTarget(VsyncId aId, DrawTarget* aTarget,
                                                const gfx::IntRect* aRect) {
   AUTO_PROFILER_TRACING("Paint", "Composite");
   AUTO_PROFILER_LABEL("CompositorBridgeParent::CompositeToTarget", GRAPHICS);
 
   MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread(),
              "Composite can only be called on the compositor thread");
   TimeStamp start = TimeStamp::Now();
 
   if (!CanComposite()) {
     TimeStamp end = TimeStamp::Now();
-    DidComposite(start, end);
+    DidComposite(aId, start, end);
     return;
   }
 
 #if defined(XP_WIN) || defined(MOZ_WIDGET_GTK)
   if (!mWaitForPluginsUntil.IsNull() && mWaitForPluginsUntil > start) {
     mHaveBlockedForPlugins = true;
     ScheduleComposition();
     return;
@@ -988,17 +989,17 @@ void CompositorBridgeParent::CompositeTo
     mLayerManager->Dump(/* aSorted = */ true);
   }
 #endif
   mLayerManager->SetDebugOverlayWantsNextFrame(false);
   mLayerManager->EndTransaction(time);
 
   if (!aTarget) {
     TimeStamp end = TimeStamp::Now();
-    DidComposite(start, end);
+    DidComposite(aId, start, end);
   }
 
   // We're not really taking advantage of the stored composite-again-time here.
   // We might be able to skip the next few composites altogether. However,
   // that's a bit complex to implement and we'll get most of the advantage
   // by skipping compositing when we detect there's nothing invalid. This is why
   // we do "composite until" rather than "composite again at".
   //
@@ -1241,17 +1242,17 @@ void CompositorBridgeParent::ShadowLayer
 
   if (root) {
     SetShadowProperties(root);
   }
   if (aInfo.scheduleComposite()) {
     ScheduleComposition();
     if (mPaused) {
       TimeStamp now = TimeStamp::Now();
-      DidComposite(now, now);
+      DidComposite(VsyncId(), now, now);
     }
   }
   mLayerManager->NotifyShadowTreeTransaction();
 }
 
 void CompositorBridgeParent::ScheduleComposite(
     LayerTransactionParent* aLayerTree) {
   ScheduleComposition();
@@ -1279,17 +1280,17 @@ bool CompositorBridgeParent::SetTestSamp
   if (testComposite) {
     AutoResolveRefLayers resolve(mCompositionManager);
     bool requestNextFrame =
         mCompositionManager->TransformShadowTree(aTime, mVsyncRate);
     if (!requestNextFrame) {
       CancelCurrentCompositeTask();
       // Pretend we composited in case someone is wating for this event.
       TimeStamp now = TimeStamp::Now();
-      DidComposite(now, now);
+      DidComposite(VsyncId(), now, now);
     }
   }
 
   return true;
 }
 
 void CompositorBridgeParent::LeaveTestMode(const LayersId& aId) {
   mTestTime = Nothing();
@@ -1309,17 +1310,17 @@ void CompositorBridgeParent::ApplyAsyncP
     TimeStamp time =
         mTestTime.valueOr(mCompositorScheduler->GetLastComposeTime());
     bool requestNextFrame =
         mCompositionManager->TransformShadowTree(time, mVsyncRate, aSkip);
     if (!requestNextFrame) {
       CancelCurrentCompositeTask();
       // Pretend we composited in case someone is waiting for this event.
       TimeStamp now = TimeStamp::Now();
-      DidComposite(now, now);
+      DidComposite(VsyncId(), now, now);
     }
   }
 }
 
 CompositorAnimationStorage* CompositorBridgeParent::GetAnimationStorage() {
   if (!mAnimationStorage) {
     mAnimationStorage = new CompositorAnimationStorage();
   }
@@ -1567,32 +1568,32 @@ CompositorBridgeParent* CompositorBridge
   if (it == sCompositorMap->end()) {
     return nullptr;
   }
   CompositorBridgeParent* retval = it->second;
   sCompositorMap->erase(it);
   return retval;
 }
 
-void CompositorBridgeParent::NotifyVsync(const TimeStamp& aTimeStamp,
+void CompositorBridgeParent::NotifyVsync(const VsyncEvent& aVsync,
                                          const LayersId& aLayersId) {
   MOZ_ASSERT(XRE_GetProcessType() == GeckoProcessType_GPU);
   MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread());
 
   MonitorAutoLock lock(*sIndirectLayerTreesLock);
   auto it = sIndirectLayerTrees.find(aLayersId);
   if (it == sIndirectLayerTrees.end()) return;
 
   CompositorBridgeParent* cbp = it->second.mParent;
   if (!cbp || !cbp->mWidget) return;
 
   RefPtr<VsyncObserver> obs = cbp->mWidget->GetVsyncObserver();
   if (!obs) return;
 
-  obs->NotifyVsync(aTimeStamp);
+  obs->NotifyVsync(aVsync);
 }
 
 mozilla::ipc::IPCResult CompositorBridgeParent::RecvNotifyChildCreated(
     const LayersId& child, CompositorOptions* aOptions) {
   MonitorAutoLock lock(*sIndirectLayerTreesLock);
   NotifyChildCreated(child);
   *aOptions = mOptions;
   return IPC_OK();
@@ -1679,18 +1680,18 @@ mozilla::ipc::IPCResult CompositorBridge
     MOZ_ASSERT(mWrBridge);
     RefPtr<wr::WebRenderAPI> api = mWrBridge->GetWebRenderAPI();
     api = api->Clone();
     wr::Epoch newEpoch = childWrBridge->UpdateWebRender(
         mWrBridge->CompositorScheduler(), api, mWrBridge->AsyncImageManager(),
         GetAnimationStorage(), mWrBridge->GetTextureFactoryIdentifier());
     // Pretend we composited, since parent CompositorBridgeParent was replaced.
     TimeStamp now = TimeStamp::Now();
-    NotifyPipelineRendered(childWrBridge->PipelineId(), newEpoch, now, now,
-                           now);
+    NotifyPipelineRendered(childWrBridge->PipelineId(), newEpoch, VsyncId(),
+                           now, now, now);
   }
 
   if (oldApzUpdater) {
     // We don't support moving a child from an APZ-enabled compositor to a
     // APZ-disabled compositor. The mOptions assertion above should already
     // ensure this, since APZ-ness is one of the things in mOptions. Note
     // however it is possible for mApzUpdater to be non-null here with
     // oldApzUpdater null, because the child may not have been previously
@@ -1971,29 +1972,24 @@ CompositorBridgeParent::LayerTreeState::
   return mCrossProcessParent;
 }
 
 MetricsSharingController*
 CompositorBridgeParent::LayerTreeState::InProcessSharingController() const {
   return mParent;
 }
 
-void CompositorBridgeParent::DidComposite(LayersId aId,
+void CompositorBridgeParent::DidComposite(const VsyncId& aId,
                                           TimeStamp& aCompositeStart,
                                           TimeStamp& aCompositeEnd) {
-  MOZ_ASSERT(aId == mRootLayerTreeID);
-  DidComposite(aCompositeStart, aCompositeEnd);
-}
-
-void CompositorBridgeParent::DidComposite(TimeStamp& aCompositeStart,
-                                          TimeStamp& aCompositeEnd) {
   if (mWrBridge) {
     MOZ_ASSERT(false);  // This should never get called for a WR compositor
   } else {
-    NotifyDidComposite(mPendingTransaction, aCompositeStart, aCompositeEnd);
+    NotifyDidComposite(mPendingTransaction, aId, aCompositeStart,
+                       aCompositeEnd);
 #if defined(ENABLE_FRAME_LATENCY_LOG)
     if (mPendingTransaction.IsValid()) {
       if (mRefreshStartTime) {
         int32_t latencyMs =
             lround((aCompositeEnd - mRefreshStartTime).ToMilliseconds());
         printf_stderr(
             "From transaction start to end of generate frame latencyMs %d this "
             "%p\n",
@@ -2012,33 +2008,35 @@ void CompositorBridgeParent::DidComposit
     mFwdTime = TimeStamp();
 #endif
     mPendingTransaction = TransactionId{0};
   }
 }
 
 void CompositorBridgeParent::NotifyPipelineRendered(
     const wr::PipelineId& aPipelineId, const wr::Epoch& aEpoch,
-    TimeStamp& aCompositeStart, TimeStamp& aRenderStart,
-    TimeStamp& aCompositeEnd, wr::RendererStats* aStats) {
+    const VsyncId& aCompositeStartId, TimeStamp& aCompositeStart,
+    TimeStamp& aRenderStart, TimeStamp& aCompositeEnd,
+    wr::RendererStats* aStats) {
   if (!mWrBridge || !mAsyncImageManager) {
     return;
   }
 
   nsTArray<FrameStats> stats;
 
   RefPtr<UiCompositorControllerParent> uiController =
       UiCompositorControllerParent::GetFromRootLayerTreeId(mRootLayerTreeID);
 
   if (mWrBridge->PipelineId() == aPipelineId) {
     mWrBridge->RemoveEpochDataPriorTo(aEpoch);
 
     if (!mPaused) {
       TransactionId transactionId = mWrBridge->FlushTransactionIdsForEpoch(
-          aEpoch, aCompositeStart, aRenderStart, aCompositeEnd, uiController);
+          aEpoch, aCompositeStartId, aCompositeStart, aRenderStart,
+          aCompositeEnd, uiController);
       Unused << SendDidComposite(LayersId{0}, transactionId, aCompositeStart,
                                  aCompositeEnd);
 
       nsTArray<ImageCompositeNotificationInfo> notifications;
       mWrBridge->ExtractImageCompositeNotifications(&notifications);
       if (!notifications.IsEmpty()) {
         Unused << ImageBridgeParent::NotifyImageComposites(notifications);
       }
@@ -2047,18 +2045,18 @@ void CompositorBridgeParent::NotifyPipel
   }
 
   auto wrBridge = mAsyncImageManager->GetWrBridge(aPipelineId);
   if (wrBridge && wrBridge->GetCompositorBridge()) {
     MOZ_ASSERT(!wrBridge->IsRootWebRenderBridgeParent());
     wrBridge->RemoveEpochDataPriorTo(aEpoch);
     if (!mPaused) {
       TransactionId transactionId = wrBridge->FlushTransactionIdsForEpoch(
-          aEpoch, aCompositeStart, aRenderStart, aCompositeEnd, uiController,
-          aStats, &stats);
+          aEpoch, aCompositeStartId, aCompositeStart, aRenderStart,
+          aCompositeEnd, uiController, aStats, &stats);
       Unused << wrBridge->GetCompositorBridge()->SendDidComposite(
           wrBridge->GetLayersId(), transactionId, aCompositeStart,
           aCompositeEnd);
     }
   }
 
   if (!stats.IsEmpty()) {
     Unused << SendNotifyFrameStats(stats);
@@ -2066,16 +2064,17 @@ void CompositorBridgeParent::NotifyPipel
 }
 
 RefPtr<AsyncImagePipelineManager>
 CompositorBridgeParent::GetAsyncImagePipelineManager() const {
   return mAsyncImageManager;
 }
 
 void CompositorBridgeParent::NotifyDidComposite(TransactionId aTransactionId,
+                                                VsyncId aId,
                                                 TimeStamp& aCompositeStart,
                                                 TimeStamp& aCompositeEnd) {
   MOZ_ASSERT(
       !mWrBridge);  // We should be going through NotifyPipelineRendered instead
 
   Unused << SendDidComposite(LayersId{0}, aTransactionId, aCompositeStart,
                              aCompositeEnd);
 
@@ -2083,23 +2082,23 @@ void CompositorBridgeParent::NotifyDidCo
     nsTArray<ImageCompositeNotificationInfo> notifications;
     mLayerManager->ExtractImageCompositeNotifications(&notifications);
     if (!notifications.IsEmpty()) {
       Unused << ImageBridgeParent::NotifyImageComposites(notifications);
     }
   }
 
   MonitorAutoLock lock(*sIndirectLayerTreesLock);
-  ForEachIndirectLayerTree(
-      [&](LayerTreeState* lts, const LayersId& aLayersId) -> void {
-        if (lts->mCrossProcessParent && lts->mParent == this) {
-          CrossProcessCompositorBridgeParent* cpcp = lts->mCrossProcessParent;
-          cpcp->DidCompositeLocked(aLayersId, aCompositeStart, aCompositeEnd);
-        }
-      });
+  ForEachIndirectLayerTree([&](LayerTreeState* lts,
+                               const LayersId& aLayersId) -> void {
+    if (lts->mCrossProcessParent && lts->mParent == this) {
+      CrossProcessCompositorBridgeParent* cpcp = lts->mCrossProcessParent;
+      cpcp->DidCompositeLocked(aLayersId, aId, aCompositeStart, aCompositeEnd);
+    }
+  });
 }
 
 void CompositorBridgeParent::InvalidateRemoteLayers() {
   MOZ_ASSERT(CompositorLoop() == MessageLoop::current());
 
   Unused << PCompositorBridgeParent::SendInvalidateLayers(LayersId{0});
 
   MonitorAutoLock lock(*sIndirectLayerTreesLock);
--- a/gfx/layers/ipc/CompositorBridgeParent.h
+++ b/gfx/layers/ipc/CompositorBridgeParent.h
@@ -36,16 +36,17 @@
 #include "mozilla/layers/MetricsSharingController.h"
 #include "mozilla/layers/PCompositorBridgeParent.h"
 #include "mozilla/layers/APZTestData.h"
 #include "mozilla/webrender/WebRenderTypes.h"
 #include "mozilla/widget/CompositorWidget.h"
 #include "nsISupportsImpl.h"
 #include "ThreadSafeRefcountingWithMainThreadDestruction.h"
 #include "mozilla/layers/UiCompositorControllerParent.h"
+#include "mozilla/VsyncDispatcher.h"
 
 class MessageLoop;
 class nsIWidget;
 
 namespace mozilla {
 
 class CancelableRunnable;
 
@@ -141,19 +142,16 @@ class CompositorBridgeParentBase : publi
   mozilla::ipc::IPCResult RecvSyncWithCompositor() override { return IPC_OK(); }
 
   mozilla::ipc::IPCResult Recv__delete__() override { return IPC_OK(); }
 
   virtual void ObserveLayersUpdate(LayersId aLayersId,
                                    LayersObserverEpoch aEpoch,
                                    bool aActive) = 0;
 
-  virtual void DidComposite(LayersId aId, TimeStamp& aCompositeStart,
-                            TimeStamp& aCompositeEnd) = 0;
-
   // HostIPCAllocator
   base::ProcessId GetChildProcessId() override;
   void NotifyNotUsed(PTextureParent* aTexture,
                      uint64_t aTransactionId) override;
   void SendAsyncMessage(
       const InfallibleTArray<AsyncParentMessageData>& aMessage) override;
 
   // ShmemAllocator
@@ -308,16 +306,17 @@ class CompositorBridgeParent final : pub
   bool DeallocPTextureParent(PTextureParent* actor) override;
 
   bool IsSameProcess() const override;
 
   void NotifyWebRenderError(wr::WebRenderError aError);
   void NotifyWebRenderContextPurge();
   void NotifyPipelineRendered(const wr::PipelineId& aPipelineId,
                               const wr::Epoch& aEpoch,
+                              const VsyncId& aCompositeStartId,
                               TimeStamp& aCompositeStart,
                               TimeStamp& aRenderStart, TimeStamp& aCompositeEnd,
                               wr::RendererStats* aStats = nullptr);
   RefPtr<AsyncImagePipelineManager> GetAsyncImagePipelineManager() const;
 
   PCompositorWidgetParent* AllocPCompositorWidgetParent(
       const CompositorWidgetInitData& aInitData) override;
   bool DeallocPCompositorWidgetParent(PCompositorWidgetParent* aActor) override;
@@ -388,18 +387,17 @@ class CompositorBridgeParent final : pub
    * Returns a pointer to the CompositorBridgeParent corresponding to the given
    * ID.
    */
   static CompositorBridgeParent* GetCompositorBridgeParent(uint64_t id);
 
   /**
    * Notify the compositor for the given layer tree that vsync has occurred.
    */
-  static void NotifyVsync(const TimeStamp& aTimeStamp,
-                          const LayersId& aLayersId);
+  static void NotifyVsync(const VsyncEvent& aVsync, const LayersId& aLayersId);
 
   /**
    * Set aController as the pan/zoom callback for the subtree referred
    * to by aLayersId.
    *
    * Must run on content main thread.
    */
   static void SetControllerForLayerTree(LayersId aLayersId,
@@ -590,17 +588,17 @@ class CompositorBridgeParent final : pub
 
  protected:
   void ForceComposition();
   void CancelCurrentCompositeTask();
 
   // CompositorVsyncSchedulerOwner
   bool IsPendingComposite() override;
   void FinishPendingComposite() override;
-  void CompositeToTarget(gfx::DrawTarget* aTarget,
+  void CompositeToTarget(VsyncId aId, gfx::DrawTarget* aTarget,
                          const gfx::IntRect* aRect = nullptr) override;
 
   bool InitializeAdvancedLayers(const nsTArray<LayersBackend>& aBackendHints,
                                 TextureFactoryIdentifier* aOutIdentifier);
   RefPtr<Compositor> NewCompositor(
       const nsTArray<LayersBackend>& aBackendHints);
 
   /**
@@ -623,21 +621,20 @@ class CompositorBridgeParent final : pub
   static void FinishShutdown();
 
   /**
    * Return true if current state allows compositing, that is
    * finishing a layers transaction.
    */
   bool CanComposite();
 
-  void DidComposite(LayersId aId, TimeStamp& aCompositeStart,
-                    TimeStamp& aCompositeEnd) override;
-  void DidComposite(TimeStamp& aCompositeStart, TimeStamp& aCompositeEnd);
+  void DidComposite(const VsyncId& aId, TimeStamp& aCompositeStart,
+                    TimeStamp& aCompositeEnd);
 
-  void NotifyDidComposite(TransactionId aTransactionId,
+  void NotifyDidComposite(TransactionId aTransactionId, VsyncId aId,
                           TimeStamp& aCompositeStart, TimeStamp& aCompositeEnd);
 
   // The indirect layer tree lock must be held before calling this function.
   // Callback should take (LayerTreeState* aState, const LayersId& aLayersId)
   template <typename Lambda>
   inline void ForEachIndirectLayerTree(const Lambda& aCallback);
 
   RefPtr<HostLayerManager> mLayerManager;
--- a/gfx/layers/ipc/CompositorVsyncScheduler.cpp
+++ b/gfx/layers/ipc/CompositorVsyncScheduler.cpp
@@ -43,23 +43,22 @@ namespace layers {
 using namespace mozilla::gfx;
 using namespace std;
 
 CompositorVsyncScheduler::Observer::Observer(CompositorVsyncScheduler* aOwner)
     : mMutex("CompositorVsyncScheduler.Observer.Mutex"), mOwner(aOwner) {}
 
 CompositorVsyncScheduler::Observer::~Observer() { MOZ_ASSERT(!mOwner); }
 
-bool CompositorVsyncScheduler::Observer::NotifyVsync(
-    TimeStamp aVsyncTimestamp) {
+bool CompositorVsyncScheduler::Observer::NotifyVsync(const VsyncEvent& aVsync) {
   MutexAutoLock lock(mMutex);
   if (!mOwner) {
     return false;
   }
-  return mOwner->NotifyVsync(aVsyncTimestamp);
+  return mOwner->NotifyVsync(aVsync);
 }
 
 void CompositorVsyncScheduler::Observer::Destroy() {
   MutexAutoLock lock(mMutex);
   mOwner = nullptr;
 }
 
 CompositorVsyncScheduler::CompositorVsyncScheduler(
@@ -102,22 +101,23 @@ void CompositorVsyncScheduler::Destroy()
   mVsyncObserver->Destroy();
   mVsyncObserver = nullptr;
 
   mCompositeRequestedAt = TimeStamp();
   CancelCurrentCompositeTask();
 }
 
 void CompositorVsyncScheduler::PostCompositeTask(
-    TimeStamp aCompositeTimestamp) {
+    VsyncId aId, TimeStamp aCompositeTimestamp) {
   MonitorAutoLock lock(mCurrentCompositeTaskMonitor);
   if (mCurrentCompositeTask == nullptr && CompositorThreadHolder::Loop()) {
-    RefPtr<CancelableRunnable> task = NewCancelableRunnableMethod<TimeStamp>(
-        "layers::CompositorVsyncScheduler::Composite", this,
-        &CompositorVsyncScheduler::Composite, aCompositeTimestamp);
+    RefPtr<CancelableRunnable> task =
+        NewCancelableRunnableMethod<VsyncId, TimeStamp>(
+            "layers::CompositorVsyncScheduler::Composite", this,
+            &CompositorVsyncScheduler::Composite, aId, aCompositeTimestamp);
     mCurrentCompositeTask = task;
     ScheduleTask(task.forget());
   }
 }
 
 void CompositorVsyncScheduler::PostVRTask(TimeStamp aTimestamp) {
   MonitorAutoLock lockVR(mCurrentVRTaskMonitor);
   if (mCurrentVRTask == nullptr && CompositorThreadHolder::Loop()) {
@@ -134,68 +134,69 @@ void CompositorVsyncScheduler::ScheduleC
   if (!mVsyncObserver) {
     // Destroy was already called on this object.
     return;
   }
 
   if (mAsapScheduling) {
     // Used only for performance testing purposes, and when recording/replaying
     // to ensure that graphics are up to date.
-    PostCompositeTask(TimeStamp::Now());
+    PostCompositeTask(VsyncId(), TimeStamp::Now());
 #ifdef MOZ_WIDGET_ANDROID
   } else if (mIsObservingVsync && mCompositeRequestedAt &&
              (TimeStamp::Now() - mCompositeRequestedAt) >=
                  mVsyncSchedulerOwner->GetVsyncInterval() * 2) {
     // uh-oh, we already requested a composite at least two vsyncs ago, and a
     // composite hasn't happened yet. It is possible that the vsync observation
     // is blocked on the main thread, so let's just composite ASAP and not
     // wait for the vsync. Note that this should only ever happen on Fennec
     // because there content runs in the same process as the compositor, and so
     // content can actually block the main thread in this process.
-    PostCompositeTask(TimeStamp::Now());
+    PostCompositeTask(VsyncId(), TimeStamp::Now());
 #endif
   } else {
     if (!mCompositeRequestedAt) {
       mCompositeRequestedAt = TimeStamp::Now();
     }
     if (!mIsObservingVsync && mCompositeRequestedAt) {
       ObserveVsync();
       // Starting to observe vsync is an async operation that goes
       // through the main thread of the UI process. It's possible that
       // we're blocking there waiting on a composite, so schedule an initial
       // one now to get things started.
-      PostCompositeTask(TimeStamp::Now());
+      PostCompositeTask(VsyncId(), TimeStamp::Now());
     }
   }
 }
 
-bool CompositorVsyncScheduler::NotifyVsync(TimeStamp aVsyncTimestamp) {
+bool CompositorVsyncScheduler::NotifyVsync(const VsyncEvent& aVsync) {
   // Called from the vsync dispatch thread. When in the GPU Process, that's
   // the same as the compositor thread.
   MOZ_ASSERT_IF(XRE_IsParentProcess(),
                 !CompositorThreadHolder::IsInCompositorThread());
   MOZ_ASSERT_IF(XRE_GetProcessType() == GeckoProcessType_GPU,
                 CompositorThreadHolder::IsInCompositorThread());
   MOZ_ASSERT(!NS_IsMainThread());
-  PostCompositeTask(aVsyncTimestamp);
-  PostVRTask(aVsyncTimestamp);
+  PostCompositeTask(aVsync.mId, aVsync.mTime);
+  PostVRTask(aVsync.mTime);
   return true;
 }
 
 void CompositorVsyncScheduler::CancelCurrentCompositeTask() {
   MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread() ||
              NS_IsMainThread());
   MonitorAutoLock lock(mCurrentCompositeTaskMonitor);
   if (mCurrentCompositeTask) {
     mCurrentCompositeTask->Cancel();
     mCurrentCompositeTask = nullptr;
   }
 }
 
-void CompositorVsyncScheduler::Composite(TimeStamp aVsyncTimestamp) {
+void CompositorVsyncScheduler::Composite(VsyncId aId,
+                                         TimeStamp aVsyncTimestamp) {
   MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread());
   MOZ_ASSERT(mVsyncSchedulerOwner);
 
   {  // scope lock
     MonitorAutoLock lock(mCurrentCompositeTaskMonitor);
     mCurrentCompositeTask = nullptr;
   }
 
@@ -216,17 +217,17 @@ void CompositorVsyncScheduler::Composite
     }
   }
 
   if (mCompositeRequestedAt || mAsapScheduling) {
     mCompositeRequestedAt = TimeStamp();
     mLastCompose = aVsyncTimestamp;
 
     // Tell the owner to do a composite
-    mVsyncSchedulerOwner->CompositeToTarget(nullptr, nullptr);
+    mVsyncSchedulerOwner->CompositeToTarget(aId, nullptr, nullptr);
 
     mVsyncNotificationsSkipped = 0;
 
     TimeDuration compositeFrameTotal = TimeStamp::Now() - aVsyncTimestamp;
     mozilla::Telemetry::Accumulate(
         mozilla::Telemetry::COMPOSITE_FRAME_ROUNDTRIP_TIME,
         compositeFrameTotal.ToMilliseconds());
   } else if (mVsyncNotificationsSkipped++ >
@@ -252,17 +253,17 @@ void CompositorVsyncScheduler::ForceComp
    * platforms, enabling/disabling vsync is not free and this oscillating
    * behavior causes a performance hit. In order to avoid this problem, we reset
    * the mVsyncNotificationsSkipped counter to keep vsync enabled.
    */
   mVsyncNotificationsSkipped = 0;
 
   mLastCompose = TimeStamp::Now();
   MOZ_ASSERT(mVsyncSchedulerOwner);
-  mVsyncSchedulerOwner->CompositeToTarget(aTarget, aRect);
+  mVsyncSchedulerOwner->CompositeToTarget(VsyncId(), aTarget, aRect);
 }
 
 bool CompositorVsyncScheduler::NeedsComposite() {
   MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread());
   return (bool)mCompositeRequestedAt;
 }
 
 bool CompositorVsyncScheduler::FlushPendingComposite() {
--- a/gfx/layers/ipc/CompositorVsyncScheduler.h
+++ b/gfx/layers/ipc/CompositorVsyncScheduler.h
@@ -43,17 +43,17 @@ class CompositorVsyncScheduler {
   explicit CompositorVsyncScheduler(
       CompositorVsyncSchedulerOwner* aVsyncSchedulerOwner,
       widget::CompositorWidget* aWidget);
 
   /**
    * Notify this class of a vsync. This will trigger a composite if one is
    * needed. This must be called from the vsync dispatch thread.
    */
-  bool NotifyVsync(TimeStamp aVsyncTimestamp);
+  bool NotifyVsync(const VsyncEvent& aVsync);
 
   /**
    * Do cleanup. This must be called on the compositor thread.
    */
   void Destroy();
 
   /**
    * Notify this class that a composition is needed. This will trigger a
@@ -103,35 +103,35 @@ class CompositorVsyncScheduler {
  private:
   virtual ~CompositorVsyncScheduler();
 
   // Schedule a task to run on the compositor thread.
   void ScheduleTask(already_AddRefed<CancelableRunnable>);
 
   // Post a task to run Composite() on the compositor thread, if there isn't
   // such a task already queued. Can be called from any thread.
-  void PostCompositeTask(TimeStamp aCompositeTimestamp);
+  void PostCompositeTask(VsyncId aId, TimeStamp aCompositeTimestamp);
 
   // Post a task to run DispatchVREvents() on the VR thread, if there isn't
   // such a task already queued. Can be called from any thread.
   void PostVRTask(TimeStamp aTimestamp);
 
   // This gets run at vsync time and "does" a composite (which really means
   // update internal state and call the owner to do the composite).
-  void Composite(TimeStamp aVsyncTimestamp);
+  void Composite(VsyncId aId, TimeStamp aVsyncTimestamp);
 
   void ObserveVsync();
   void UnobserveVsync();
 
   void DispatchVREvents(TimeStamp aVsyncTimestamp);
 
   class Observer final : public VsyncObserver {
    public:
     explicit Observer(CompositorVsyncScheduler* aOwner);
-    virtual bool NotifyVsync(TimeStamp aVsyncTimestamp) override;
+    virtual bool NotifyVsync(const VsyncEvent& aVsync) override;
     void Destroy();
 
    private:
     virtual ~Observer();
 
     Mutex mMutex;
     // Hold raw pointer to avoid mutual reference.
     CompositorVsyncScheduler* mOwner;
--- a/gfx/layers/ipc/CompositorVsyncSchedulerOwner.h
+++ b/gfx/layers/ipc/CompositorVsyncSchedulerOwner.h
@@ -2,29 +2,31 @@
 /* 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 mozilla_layers_CompositorVsyncSchedulerOwner_h
 #define mozilla_layers_CompositorVsyncSchedulerOwner_h
 
+#include "mozilla/VsyncDispatcher.h"
+
 namespace mozilla {
 
 namespace gfx {
 class DrawTarget;
 }  // namespace gfx
 
 namespace layers {
 
 class CompositorVsyncSchedulerOwner {
  public:
   virtual bool IsPendingComposite() = 0;
   virtual void FinishPendingComposite() = 0;
-  virtual void CompositeToTarget(gfx::DrawTarget* aTarget,
+  virtual void CompositeToTarget(VsyncId aId, gfx::DrawTarget* aTarget,
                                  const gfx::IntRect* aRect = nullptr) = 0;
   virtual TimeDuration GetVsyncInterval() const = 0;
 };
 
 }  // namespace layers
 }  // namespace mozilla
 
 #endif  // mozilla_layers_CompositorVsyncSchedulerOwner_h
--- a/gfx/layers/ipc/CrossProcessCompositorBridgeParent.cpp
+++ b/gfx/layers/ipc/CrossProcessCompositorBridgeParent.cpp
@@ -384,32 +384,28 @@ void CrossProcessCompositorBridgeParent:
         MakeUnique<ContentBuildPayload>(aInfo.transactionStart(), endTime));
   }
 #endif
   Telemetry::Accumulate(
       Telemetry::CONTENT_FULL_PAINT_TIME,
       static_cast<uint32_t>(
           (endTime - aInfo.transactionStart()).ToMilliseconds()));
 
-  aLayerTree->SetPendingTransactionId(aInfo.id(), aInfo.refreshStart(),
-                                      aInfo.transactionStart(), aInfo.url(),
-                                      aInfo.fwdTime());
-}
-
-void CrossProcessCompositorBridgeParent::DidComposite(
-    LayersId aId, TimeStamp& aCompositeStart, TimeStamp& aCompositeEnd) {
-  MonitorAutoLock lock(*sIndirectLayerTreesLock);
-  DidCompositeLocked(aId, aCompositeStart, aCompositeEnd);
+  aLayerTree->SetPendingTransactionId(
+      aInfo.id(), aInfo.vsyncId(), aInfo.refreshStart(),
+      aInfo.transactionStart(), aInfo.url(), aInfo.fwdTime());
 }
 
 void CrossProcessCompositorBridgeParent::DidCompositeLocked(
-    LayersId aId, TimeStamp& aCompositeStart, TimeStamp& aCompositeEnd) {
+    LayersId aId, const VsyncId& aVsyncId, TimeStamp& aCompositeStart,
+    TimeStamp& aCompositeEnd) {
   sIndirectLayerTreesLock->AssertCurrentThreadOwns();
   if (LayerTransactionParent* layerTree = sIndirectLayerTrees[aId].mLayerTree) {
-    TransactionId transactionId = layerTree->FlushTransactionId(aCompositeEnd);
+    TransactionId transactionId =
+        layerTree->FlushTransactionId(aVsyncId, aCompositeEnd);
     if (transactionId.IsValid()) {
       Unused << SendDidComposite(aId, transactionId, aCompositeStart,
                                  aCompositeEnd);
     }
   } else if (sIndirectLayerTrees[aId].mWrBridge) {
     MOZ_ASSERT(false);  // this should never get called for a WR compositor
   }
 }
--- a/gfx/layers/ipc/CrossProcessCompositorBridgeParent.h
+++ b/gfx/layers/ipc/CrossProcessCompositorBridgeParent.h
@@ -132,20 +132,18 @@ class CrossProcessCompositorBridgeParent
       LayerTransactionParent* aParent) override;
   mozilla::ipc::IPCResult RecvRemotePluginsReady() override {
     return IPC_FAIL_NO_REASON(this);
   }
 
   // Use DidCompositeLocked if you already hold a lock on
   // sIndirectLayerTreesLock; Otherwise use DidComposite, which would request
   // the lock automatically.
-  void DidCompositeLocked(LayersId aId, TimeStamp& aCompositeStart,
-                          TimeStamp& aCompositeEnd);
-  void DidComposite(LayersId aId, TimeStamp& aCompositeStart,
-                    TimeStamp& aCompositeEnd) override;
+  void DidCompositeLocked(LayersId aId, const VsyncId& aVsyncId,
+                          TimeStamp& aCompositeStart, TimeStamp& aCompositeEnd);
 
   PTextureParent* AllocPTextureParent(
       const SurfaceDescriptor& aSharedData, const ReadLockDescriptor& aReadLock,
       const LayersBackend& aLayersBackend, const TextureFlags& aFlags,
       const LayersId& aId, const uint64_t& aSerial,
       const wr::MaybeExternalImageId& aExternalImageId) override;
 
   bool DeallocPTextureParent(PTextureParent* actor) override;
--- a/gfx/layers/ipc/LayerTransactionParent.cpp
+++ b/gfx/layers/ipc/LayerTransactionParent.cpp
@@ -34,16 +34,18 @@
 #include "nsMathUtils.h"      // for NS_round
 #include "nsPoint.h"          // for nsPoint
 #include "nsTArray.h"         // for nsTArray, nsTArray_Impl, etc
 #include "TreeTraversal.h"    // for ForEachNode
 #include "GeckoProfiler.h"
 #include "mozilla/layers/TextureHost.h"
 #include "mozilla/layers/AsyncCompositionManager.h"
 
+using mozilla::Telemetry::LABELS_CONTENT_FRAME_TIME_REASON;
+
 namespace mozilla {
 namespace layers {
 
 //--------------------------------------------------
 // LayerTransactionParent
 LayerTransactionParent::LayerTransactionParent(
     HostLayerManager* aManager, CompositorBridgeParentBase* aBridge,
     CompositorAnimationStorage* aAnimStorage, LayersId aId,
@@ -879,22 +881,59 @@ void LayerTransactionParent::DeallocShme
   PLayerTransactionParent::DeallocShmem(aShmem);
 }
 
 bool LayerTransactionParent::IsSameProcess() const {
   return OtherPid() == base::GetCurrentProcId();
 }
 
 TransactionId LayerTransactionParent::FlushTransactionId(
-    TimeStamp& aCompositeEnd) {
+    const VsyncId& aId, TimeStamp& aCompositeEnd) {
   if (mId.IsValid() && mPendingTransaction.IsValid() && !mVsyncRate.IsZero()) {
     double latencyMs = (aCompositeEnd - mTxnStartTime).ToMilliseconds();
     double latencyNorm = latencyMs / mVsyncRate.ToMilliseconds();
     int32_t fracLatencyNorm = lround(latencyNorm * 100.0);
     Telemetry::Accumulate(Telemetry::CONTENT_FRAME_TIME, fracLatencyNorm);
+
+    // Record CONTENT_FRAME_TIME_REASON. See
+    // WebRenderBridgeParent::FlushTransactionIdsForEpoch for more details.
+    //
+    // Note that deseralizing a layers update (RecvUpdate) can delay the receipt
+    // of the composite vsync message
+    // (CompositorBridgeParent::CompositeToTarget), since they're using the same
+    // thread. This can mean that compositing might start significantly late,
+    // but this code will still detect it as having successfully started on the
+    // right vsync (which is somewhat correct). We'd now have reduced time left
+    // in the vsync interval to finish compositing, so the chances of a missed
+    // frame increases. This is effectively including the RecvUpdate work as
+    // part of the 'compositing' phase for this metric, but it isn't included in
+    // COMPOSITE_TIME, and *is* included in CONTENT_FULL_PAINT_TIME.
+    latencyMs = (aCompositeEnd - mRefreshStartTime).ToMilliseconds();
+    latencyNorm = latencyMs / mVsyncRate.ToMilliseconds();
+    fracLatencyNorm = lround(latencyNorm * 100.0);
+    if (fracLatencyNorm < 200) {
+      // Success
+      Telemetry::AccumulateCategorical(
+          LABELS_CONTENT_FRAME_TIME_REASON::OnTime);
+    } else {
+      if (mTxnVsyncId == VsyncId() || aId == VsyncId() || mTxnVsyncId >= aId) {
+        // Vsync ids are nonsensical, possibly something got trigged from
+        // outside vsync?
+        Telemetry::AccumulateCategorical(
+            LABELS_CONTENT_FRAME_TIME_REASON::NoVsync);
+      } else if (aId - mTxnVsyncId > 1) {
+        // Composite started late (and maybe took too long as well)
+        Telemetry::AccumulateCategorical(
+            LABELS_CONTENT_FRAME_TIME_REASON::MissedComposite);
+      } else {
+        // Composite start on time, but must have taken too long.
+        Telemetry::AccumulateCategorical(
+            LABELS_CONTENT_FRAME_TIME_REASON::SlowComposite);
+      }
+    }
   }
 
 #if defined(ENABLE_FRAME_LATENCY_LOG)
   if (mPendingTransaction.IsValid()) {
     if (mRefreshStartTime) {
       int32_t latencyMs =
           lround((aCompositeEnd - mRefreshStartTime).ToMilliseconds());
       printf_stderr(
--- a/gfx/layers/ipc/LayerTransactionParent.h
+++ b/gfx/layers/ipc/LayerTransactionParent.h
@@ -67,28 +67,30 @@ class LayerTransactionParent final : pub
   bool AllocUnsafeShmem(size_t aSize, ipc::SharedMemory::SharedMemoryType aType,
                         ipc::Shmem* aShmem) override;
 
   void DeallocShmem(ipc::Shmem& aShmem) override;
 
   bool IsSameProcess() const override;
 
   const TransactionId& GetPendingTransactionId() { return mPendingTransaction; }
-  void SetPendingTransactionId(TransactionId aId,
+  void SetPendingTransactionId(TransactionId aId, const VsyncId& aVsyncId,
                                const TimeStamp& aRefreshStartTime,
                                const TimeStamp& aTxnStartTime,
                                const nsCString& aURL,
                                const TimeStamp& aFwdTime) {
     mPendingTransaction = aId;
+    mTxnVsyncId = aVsyncId;
     mRefreshStartTime = aRefreshStartTime;
     mTxnStartTime = aTxnStartTime;
     mTxnURL = aURL;
     mFwdTime = aFwdTime;
   }
-  TransactionId FlushTransactionId(TimeStamp& aCompositeEnd);
+  TransactionId FlushTransactionId(const VsyncId& aId,
+                                   TimeStamp& aCompositeEnd);
 
   // CompositableParentManager
   void SendAsyncMessage(
       const InfallibleTArray<AsyncParentMessageData>& aMessage) override;
 
   void SendPendingAsyncMessages() override;
 
   void SetAboutToSendAsyncMessages() override;
@@ -197,16 +199,17 @@ class LayerTransactionParent final : pub
   // mParentEpoch is the latest epoch value that we have told TabParent about
   // (via ObserveLayerUpdate).
   LayersObserverEpoch mChildEpoch;
   LayersObserverEpoch mParentEpoch;
 
   TimeDuration mVsyncRate;
 
   TransactionId mPendingTransaction;
+  VsyncId mTxnVsyncId;
   TimeStamp mRefreshStartTime;
   TimeStamp mTxnStartTime;
   TimeStamp mFwdTime;
   nsCString mTxnURL;
 
   // When the widget/frame/browser stuff in this process begins its
   // destruction process, we need to Disconnect() all the currently
   // live shadow layers, because some of them might be orphaned from
--- a/gfx/layers/ipc/LayersMessageUtils.h
+++ b/gfx/layers/ipc/LayersMessageUtils.h
@@ -19,33 +19,53 @@
 #include "mozilla/layers/CompositorTypes.h"
 #include "mozilla/layers/FocusTarget.h"
 #include "mozilla/layers/GeckoContentController.h"
 #include "mozilla/layers/KeyboardMap.h"
 #include "mozilla/layers/LayerAttributes.h"
 #include "mozilla/layers/LayersTypes.h"
 #include "mozilla/layers/RefCountedShmem.h"
 #include "mozilla/layers/RepaintRequest.h"
+#include "VsyncSource.h"
 #include "mozilla/Move.h"
 
 #include <stdint.h>
 
 #ifdef _MSC_VER
 #pragma warning(disable : 4800)
 #endif
 
 namespace IPC {
 
 template <>
 struct ParamTraits<mozilla::layers::LayersId>
     : public PlainOldDataSerializer<mozilla::layers::LayersId> {};
 
+template <typename T>
+struct ParamTraits<mozilla::layers::BaseTransactionId<T>>
+    : public PlainOldDataSerializer<mozilla::layers::BaseTransactionId<T>> {};
+
 template <>
-struct ParamTraits<mozilla::layers::TransactionId>
-    : public PlainOldDataSerializer<mozilla::layers::TransactionId> {};
+struct ParamTraits<mozilla::VsyncId>
+    : public PlainOldDataSerializer<mozilla::VsyncId> {};
+
+template <>
+struct ParamTraits<mozilla::VsyncEvent> {
+  typedef mozilla::VsyncEvent paramType;
+
+  static void Write(Message* msg, const paramType& param) {
+    WriteParam(msg, param.mId);
+    WriteParam(msg, param.mTime);
+  }
+  static bool Read(const Message* msg, PickleIterator* iter,
+                   paramType* result) {
+    return ReadParam(msg, iter, &result->mId) &&
+           ReadParam(msg, iter, &result->mTime);
+  }
+};
 
 template <>
 struct ParamTraits<mozilla::layers::LayersObserverEpoch>
     : public PlainOldDataSerializer<mozilla::layers::LayersObserverEpoch> {};
 
 template <>
 struct ParamTraits<mozilla::layers::LayersBackend>
     : public ContiguousEnumSerializer<
--- a/gfx/layers/ipc/LayersMessages.ipdlh
+++ b/gfx/layers/ipc/LayersMessages.ipdlh
@@ -47,16 +47,17 @@ using mozilla::layers::ScrollableLayerGu
 using mozilla::layers::LayersBackend from "mozilla/layers/LayersTypes.h";
 using mozilla::layers::LayerHandle from "mozilla/layers/LayersTypes.h";
 using mozilla::layers::CompositableHandle from "mozilla/layers/LayersTypes.h";
 using mozilla::layers::SimpleLayerAttributes from "mozilla/layers/LayerAttributes.h";
 using mozilla::CrossProcessSemaphoreHandle from "mozilla/ipc/CrossProcessSemaphore.h";
 using struct mozilla::void_t from "ipc/IPCMessageUtils.h";
 using mozilla::layers::LayersId from "mozilla/layers/LayersTypes.h";
 using mozilla::layers::TransactionId from "mozilla/layers/LayersTypes.h";
+using mozilla::VsyncId from "mozilla/VsyncDispatcher.h";
 
 namespace mozilla {
 namespace layers {
 
 struct TargetConfig {
   IntRect naturalBounds;
   ScreenRotation rotation;
   ScreenOrientation orientation;
@@ -549,16 +550,17 @@ struct TransactionInfo
   TransactionId id;
   TargetConfig targetConfig;
   PluginWindowData[] plugins;
   bool isFirstPaint;
   FocusTarget focusTarget;
   bool scheduleComposite;
   uint32_t paintSequenceNumber;
   bool isRepeatTransaction;
+  VsyncId vsyncId;
   TimeStamp refreshStart;
   TimeStamp transactionStart;
   nsCString url;
   TimeStamp fwdTime;
 };
 
 union MaybeTransform {
   Matrix4x4;
--- a/gfx/layers/ipc/PWebRenderBridge.ipdl
+++ b/gfx/layers/ipc/PWebRenderBridge.ipdl
@@ -23,16 +23,17 @@ using mozilla::layers::CompositableHandl
 using mozilla::wr::BuiltDisplayListDescriptor from "mozilla/webrender/webrender_ffi.h";
 using mozilla::wr::IdNamespace from "mozilla/webrender/WebRenderTypes.h";
 using mozilla::wr::MaybeIdNamespace from "mozilla/webrender/WebRenderTypes.h";
 using mozilla::wr::ExternalImageKeyPair from "mozilla/webrender/WebRenderTypes.h";
 using mozilla::layers::WebRenderScrollData from "mozilla/layers/WebRenderScrollData.h";
 using mozilla::layers::FocusTarget from "mozilla/layers/FocusTarget.h";
 using mozilla::layers::LayersObserverEpoch from "mozilla/layers/LayersTypes.h";
 using mozilla::layers::TransactionId from "mozilla/layers/LayersTypes.h";
+using mozilla::VsyncId from "mozilla/VsyncDispatcher.h";
 
 namespace mozilla {
 namespace layers {
 
 sync protocol PWebRenderBridge
 {
   manager PCompositorBridge;
 
@@ -43,21 +44,21 @@ parent:
   async NewCompositable(CompositableHandle handle, TextureInfo info);
   async ReleaseCompositable(CompositableHandle compositable);
 
   async DeleteCompositorAnimations(uint64_t[] aIds);
   async SetDisplayList(IntSize aSize, WebRenderParentCommand[] commands, OpDestroy[] toDestroy, uint64_t fwdTransactionId, TransactionId transactionId,
                        LayoutSize aContentSize, ByteBuf aDL, BuiltDisplayListDescriptor aDLDesc,
                        WebRenderScrollData aScrollData,
                        OpUpdateResource[] aResourceUpdates, RefCountedShmem[] aSmallShmems, Shmem[] aLargeShmems,
-                       IdNamespace aIdNamespace, bool containsSVGGroup, TimeStamp refreshStartTime, TimeStamp txnStartTime, nsCString txnURL, TimeStamp fwdTime);
+                       IdNamespace aIdNamespace, bool containsSVGGroup, VsyncId vsyncId, TimeStamp refreshStartTime, TimeStamp txnStartTime, nsCString txnURL, TimeStamp fwdTime);
   async EmptyTransaction(FocusTarget focusTarget, ScrollUpdatesMap scrollUpdates, uint32_t aPaintSequenceNumber,
                          WebRenderParentCommand[] commands, OpDestroy[] toDestroy, uint64_t fwdTransactionId, TransactionId transactionId,
                          OpUpdateResource[] aResourceUpdates, RefCountedShmem[] aSmallShmems, Shmem[] aLargeShmems,
-                         IdNamespace aIdNamespace, TimeStamp refreshStartTime, TimeStamp txnStartTime, nsCString txnURL, TimeStamp fwdTime);
+                         IdNamespace aIdNamespace, VsyncId vsyncId, TimeStamp refreshStartTime, TimeStamp txnStartTime, nsCString txnURL, TimeStamp fwdTime);
   async SetFocusTarget(FocusTarget focusTarget);
   async UpdateResources(OpUpdateResource[] aResourceUpdates, RefCountedShmem[] aSmallShmems, Shmem[] aLargeShmems);
   async ParentCommands(WebRenderParentCommand[] commands);
   sync GetSnapshot(PTexture texture);
   async SetLayersObserverEpoch(LayersObserverEpoch childEpoch);
   async ClearCachedResources();
   // Schedule a composite if one isn't already scheduled.
   async ScheduleComposite();
--- a/gfx/layers/ipc/ShadowLayers.cpp
+++ b/gfx/layers/ipc/ShadowLayers.cpp
@@ -523,17 +523,18 @@ void ShadowLayerForwarder::SendPaintTime
   if (!IPCOpen() || !mShadowManager->SendPaintTime(aId, aPaintTime)) {
     NS_WARNING("Could not send paint times over IPC");
   }
 }
 
 bool ShadowLayerForwarder::EndTransaction(
     const nsIntRegion& aRegionToClear, TransactionId aId,
     bool aScheduleComposite, uint32_t aPaintSequenceNumber,
-    bool aIsRepeatTransaction, const mozilla::TimeStamp& aRefreshStart,
+    bool aIsRepeatTransaction, const mozilla::VsyncId& aVsyncId,
+    const mozilla::TimeStamp& aRefreshStart,
     const mozilla::TimeStamp& aTransactionStart, const nsCString& aURL,
     bool* aSent) {
   *aSent = false;
 
   TransactionInfo info;
 
   MOZ_ASSERT(IPCOpen(), "no manager to forward to");
   if (!IPCOpen()) {
@@ -665,16 +666,17 @@ bool ShadowLayerForwarder::EndTransactio
   info.fwdTransactionId() = GetFwdTransactionId();
   info.id() = aId;
   info.plugins() = mPluginWindowData;
   info.isFirstPaint() = mIsFirstPaint;
   info.focusTarget() = mFocusTarget;
   info.scheduleComposite() = aScheduleComposite;
   info.paintSequenceNumber() = aPaintSequenceNumber;
   info.isRepeatTransaction() = aIsRepeatTransaction;
+  info.vsyncId() = aVsyncId;
   info.refreshStart() = aRefreshStart;
   info.transactionStart() = aTransactionStart;
   info.url() = aURL;
 #if defined(ENABLE_FRAME_LATENCY_LOG)
   info.fwdTime() = TimeStamp::Now();
 #endif
 
   TargetConfig targetConfig(mTxn->mTargetBounds, mTxn->mTargetRotation,
--- a/gfx/layers/ipc/ShadowLayers.h
+++ b/gfx/layers/ipc/ShadowLayers.h
@@ -247,16 +247,17 @@ class ShadowLayerForwarder final : publi
   /**
    * End the current transaction and forward it to LayerManagerComposite.
    * |aReplies| are directions from the LayerManagerComposite to the
    * caller of EndTransaction().
    */
   bool EndTransaction(const nsIntRegion& aRegionToClear, TransactionId aId,
                       bool aScheduleComposite, uint32_t aPaintSequenceNumber,
                       bool aIsRepeatTransaction,
+                      const mozilla::VsyncId& aVsyncId,
                       const mozilla::TimeStamp& aRefreshStart,
                       const mozilla::TimeStamp& aTransactionStart,
                       const nsCString& aURL, bool* aSent);
 
   /**
    * Set an actor through which layer updates will be pushed.
    */
   void SetShadowManager(PLayerTransactionChild* aShadowManager);
--- a/gfx/layers/wr/WebRenderBridgeChild.cpp
+++ b/gfx/layers/wr/WebRenderBridgeChild.cpp
@@ -100,47 +100,50 @@ void WebRenderBridgeChild::UpdateResourc
 
   this->SendUpdateResources(resourceUpdates, smallShmems, largeShmems);
 }
 
 void WebRenderBridgeChild::EndTransaction(
     const wr::LayoutSize& aContentSize, wr::BuiltDisplayList& aDL,
     wr::IpcResourceUpdateQueue& aResources, const gfx::IntSize& aSize,
     TransactionId aTransactionId, const WebRenderScrollData& aScrollData,
-    bool aContainsSVGGroup, const mozilla::TimeStamp& aRefreshStartTime,
+    bool aContainsSVGGroup, const mozilla::VsyncId& aVsyncId,
+    const mozilla::TimeStamp& aRefreshStartTime,
     const mozilla::TimeStamp& aTxnStartTime, const nsCString& aTxnURL) {
   MOZ_ASSERT(!mDestroyed);
   MOZ_ASSERT(mIsInTransaction);
 
   ByteBuf dlData(aDL.dl.inner.data, aDL.dl.inner.length, aDL.dl.inner.capacity);
   aDL.dl.inner.capacity = 0;
   aDL.dl.inner.data = nullptr;
 
   TimeStamp fwdTime = TimeStamp::Now();
 
   nsTArray<OpUpdateResource> resourceUpdates;
   nsTArray<RefCountedShmem> smallShmems;
   nsTArray<ipc::Shmem> largeShmems;
   aResources.Flush(resourceUpdates, smallShmems, largeShmems);
 
-  this->SendSetDisplayList(
-      aSize, mParentCommands, mDestroyedActors, GetFwdTransactionId(),
-      aTransactionId, aContentSize, dlData, aDL.dl_desc, aScrollData,
-      resourceUpdates, smallShmems, largeShmems, mIdNamespace,
-      aContainsSVGGroup, aRefreshStartTime, aTxnStartTime, aTxnURL, fwdTime);
+  this->SendSetDisplayList(aSize, mParentCommands, mDestroyedActors,
+                           GetFwdTransactionId(), aTransactionId, aContentSize,
+                           dlData, aDL.dl_desc, aScrollData, resourceUpdates,
+                           smallShmems, largeShmems, mIdNamespace,
+                           aContainsSVGGroup, aVsyncId, aRefreshStartTime,
+                           aTxnStartTime, aTxnURL, fwdTime);
 
   mParentCommands.Clear();
   mDestroyedActors.Clear();
   mIsInTransaction = false;
 }
 
 void WebRenderBridgeChild::EndEmptyTransaction(
     const FocusTarget& aFocusTarget, const ScrollUpdatesMap& aUpdates,
     Maybe<wr::IpcResourceUpdateQueue>& aResources,
     uint32_t aPaintSequenceNumber, TransactionId aTransactionId,
+    const mozilla::VsyncId& aVsyncId,
     const mozilla::TimeStamp& aRefreshStartTime,
     const mozilla::TimeStamp& aTxnStartTime, const nsCString& aTxnURL) {
   MOZ_ASSERT(!mDestroyed);
   MOZ_ASSERT(mIsInTransaction);
 
   TimeStamp fwdTime = TimeStamp::Now();
 
   nsTArray<OpUpdateResource> resourceUpdates;
@@ -149,18 +152,18 @@ void WebRenderBridgeChild::EndEmptyTrans
   if (aResources) {
     aResources->Flush(resourceUpdates, smallShmems, largeShmems);
     aResources.reset();
   }
 
   this->SendEmptyTransaction(
       aFocusTarget, aUpdates, aPaintSequenceNumber, mParentCommands,
       mDestroyedActors, GetFwdTransactionId(), aTransactionId, resourceUpdates,
-      smallShmems, largeShmems, mIdNamespace, aRefreshStartTime, aTxnStartTime,
-      aTxnURL, fwdTime);
+      smallShmems, largeShmems, mIdNamespace, aVsyncId, aRefreshStartTime,
+      aTxnStartTime, aTxnURL, fwdTime);
   mParentCommands.Clear();
   mDestroyedActors.Clear();
   mIsInTransaction = false;
 }
 
 void WebRenderBridgeChild::ProcessWebRenderParentCommands() {
   MOZ_ASSERT(!mDestroyed);
 
--- a/gfx/layers/wr/WebRenderBridgeChild.h
+++ b/gfx/layers/wr/WebRenderBridgeChild.h
@@ -61,27 +61,31 @@ class WebRenderBridgeChild final : publi
 
  public:
   explicit WebRenderBridgeChild(const wr::PipelineId& aPipelineId);
 
   void AddWebRenderParentCommand(const WebRenderParentCommand& aCmd);
 
   void UpdateResources(wr::IpcResourceUpdateQueue& aResources);
   void BeginTransaction();
-  void EndTransaction(
-      const wr::LayoutSize& aContentSize, wr::BuiltDisplayList& dl,
-      wr::IpcResourceUpdateQueue& aResources, const gfx::IntSize& aSize,
-      TransactionId aTransactionId, const WebRenderScrollData& aScrollData,
-      bool aContainsSVGroup, const mozilla::TimeStamp& aRefreshStartTime,
-      const mozilla::TimeStamp& aTxnStartTime, const nsCString& aTxtURL);
+  void EndTransaction(const wr::LayoutSize& aContentSize,
+                      wr::BuiltDisplayList& dl,
+                      wr::IpcResourceUpdateQueue& aResources,
+                      const gfx::IntSize& aSize, TransactionId aTransactionId,
+                      const WebRenderScrollData& aScrollData,
+                      bool aContainsSVGroup, const mozilla::VsyncId& aVsyncId,
+                      const mozilla::TimeStamp& aRefreshStartTime,
+                      const mozilla::TimeStamp& aTxnStartTime,
+                      const nsCString& aTxtURL);
   void EndEmptyTransaction(const FocusTarget& aFocusTarget,
                            const ScrollUpdatesMap& aUpdates,
                            Maybe<wr::IpcResourceUpdateQueue>& aResources,
                            uint32_t aPaintSequenceNumber,
                            TransactionId aTransactionId,
+                           const mozilla::VsyncId& aVsyncId,
                            const mozilla::TimeStamp& aRefreshStartTime,
                            const mozilla::TimeStamp& aTxnStartTime,
                            const nsCString& aTxtURL);
   void ProcessWebRenderParentCommands();
 
   CompositorBridgeChild* GetCompositorBridgeChild();
 
   wr::PipelineId GetPipeline() { return mPipelineId; }
--- a/gfx/layers/wr/WebRenderBridgeParent.cpp
+++ b/gfx/layers/wr/WebRenderBridgeParent.cpp
@@ -30,16 +30,18 @@
 #include "mozilla/layers/WebRenderImageHost.h"
 #include "mozilla/layers/WebRenderTextureHost.h"
 #include "mozilla/Telemetry.h"
 #include "mozilla/TimeStamp.h"
 #include "mozilla/Unused.h"
 #include "mozilla/webrender/RenderThread.h"
 #include "mozilla/widget/CompositorWidget.h"
 
+using mozilla::Telemetry::LABELS_CONTENT_FRAME_TIME_REASON;
+
 #ifdef MOZ_GECKO_PROFILER
 #include "ProfilerMarkerPayload.h"
 #endif
 
 bool is_in_main_thread() { return NS_IsMainThread(); }
 
 bool is_in_compositor_thread() {
   return mozilla::layers::CompositorThreadHolder::IsInCompositorThread();
@@ -849,19 +851,19 @@ mozilla::ipc::IPCResult WebRenderBridgeP
     InfallibleTArray<WebRenderParentCommand>&& aCommands,
     InfallibleTArray<OpDestroy>&& aToDestroy, const uint64_t& aFwdTransactionId,
     const TransactionId& aTransactionId, const wr::LayoutSize& aContentSize,
     ipc::ByteBuf&& dl, const wr::BuiltDisplayListDescriptor& dlDesc,
     const WebRenderScrollData& aScrollData,
     nsTArray<OpUpdateResource>&& aResourceUpdates,
     nsTArray<RefCountedShmem>&& aSmallShmems,
     nsTArray<ipc::Shmem>&& aLargeShmems, const wr::IdNamespace& aIdNamespace,
-    const bool& aContainsSVGGroup, const TimeStamp& aRefreshStartTime,
-    const TimeStamp& aTxnStartTime, const nsCString& aTxnURL,
-    const TimeStamp& aFwdTime) {
+    const bool& aContainsSVGGroup, const VsyncId& aVsyncId,
+    const TimeStamp& aRefreshStartTime, const TimeStamp& aTxnStartTime,
+    const nsCString& aTxnURL, const TimeStamp& aFwdTime) {
   if (mDestroyed) {
     for (const auto& op : aToDestroy) {
       DestroyActor(op);
     }
     return IPC_OK();
   }
 
   AUTO_PROFILER_TRACING("Paint", "SetDisplayList");
@@ -940,27 +942,28 @@ mozilla::ipc::IPCResult WebRenderBridgeP
 
     // We will schedule generating a frame after the scene
     // build is done, so we don't need to do it here.
   } else if (observeLayersUpdate) {
     mCompositorBridge->ObserveLayersUpdate(GetLayersId(),
                                            mChildLayersObserverEpoch, true);
   }
 
-  HoldPendingTransactionId(wrEpoch, aTransactionId, aContainsSVGGroup,
+  HoldPendingTransactionId(wrEpoch, aTransactionId, aContainsSVGGroup, aVsyncId,
                            aRefreshStartTime, aTxnStartTime, aTxnURL, aFwdTime,
                            mIsFirstPaint);
   mIsFirstPaint = false;
 
   if (!validTransaction) {
     // Pretend we composited since someone is wating for this event,
     // though DisplayList was not pushed to webrender.
     if (CompositorBridgeParent* cbp = GetRootCompositorBridgeParent()) {
       TimeStamp now = TimeStamp::Now();
-      cbp->NotifyPipelineRendered(mPipelineId, wrEpoch, now, now, now);
+      cbp->NotifyPipelineRendered(mPipelineId, wrEpoch, VsyncId(), now, now,
+                                  now);
     }
   }
 
   wr::IpcResourceUpdateQueue::ReleaseShmems(this, aSmallShmems);
   wr::IpcResourceUpdateQueue::ReleaseShmems(this, aLargeShmems);
   return IPC_OK();
 }
 
@@ -968,18 +971,19 @@ mozilla::ipc::IPCResult WebRenderBridgeP
     const FocusTarget& aFocusTarget, const ScrollUpdatesMap& aUpdates,
     const uint32_t& aPaintSequenceNumber,
     InfallibleTArray<WebRenderParentCommand>&& aCommands,
     InfallibleTArray<OpDestroy>&& aToDestroy, const uint64_t& aFwdTransactionId,
     const TransactionId& aTransactionId,
     nsTArray<OpUpdateResource>&& aResourceUpdates,
     nsTArray<RefCountedShmem>&& aSmallShmems,
     nsTArray<ipc::Shmem>&& aLargeShmems, const wr::IdNamespace& aIdNamespace,
-    const TimeStamp& aRefreshStartTime, const TimeStamp& aTxnStartTime,
-    const nsCString& aTxnURL, const TimeStamp& aFwdTime) {
+    const VsyncId& aVsyncId, const TimeStamp& aRefreshStartTime,
+    const TimeStamp& aTxnStartTime, const nsCString& aTxnURL,
+    const TimeStamp& aFwdTime) {
   if (mDestroyed) {
     for (const auto& op : aToDestroy) {
       DestroyActor(op);
     }
     return IPC_OK();
   }
 
   AUTO_PROFILER_TRACING("Paint", "EmptyTransaction");
@@ -1053,34 +1057,35 @@ mozilla::ipc::IPCResult WebRenderBridgeP
     // If there are no pending transactions and we're not going to do a
     // composite, then we leave sendDidComposite as true so we just send
     // the DidComposite notification now.
     sendDidComposite = false;
   }
 
   // Only register a value for CONTENT_FRAME_TIME telemetry if we actually drew
   // something. It is for consistency with disabling WebRender.
-  HoldPendingTransactionId(mWrEpoch, aTransactionId, false, aRefreshStartTime,
-                           aTxnStartTime, aTxnURL, aFwdTime,
+  HoldPendingTransactionId(mWrEpoch, aTransactionId, false, aVsyncId,
+                           aRefreshStartTime, aTxnStartTime, aTxnURL, aFwdTime,
                            /* aIsFirstPaint */ false,
                            /* aUseForTelemetry */ scheduleComposite);
 
   if (scheduleComposite) {
     // This is actually not necessary, since ScheduleGenerateFrame() is
     // triggered via SceneBuilder thread. But if we remove it, it causes talos
     // regression. The SceneBuilder thread seems not trigger next vsync right
     // away. For now, we call ScheduleGenerateFrame() here.
     ScheduleGenerateFrame();
   } else if (sendDidComposite) {
     // The only thing in the pending transaction id queue should be the entry
     // we just added, and now we're going to pretend we rendered it
     MOZ_ASSERT(mPendingTransactionIds.size() == 1);
     if (CompositorBridgeParent* cbp = GetRootCompositorBridgeParent()) {
       TimeStamp now = TimeStamp::Now();
-      cbp->NotifyPipelineRendered(mPipelineId, mWrEpoch, now, now, now);
+      cbp->NotifyPipelineRendered(mPipelineId, mWrEpoch, VsyncId(), now, now,
+                                  now);
     }
   }
 
   wr::IpcResourceUpdateQueue::ReleaseShmems(this, aSmallShmems);
   wr::IpcResourceUpdateQueue::ReleaseShmems(this, aLargeShmems);
   return IPC_OK();
 }
 
@@ -1212,17 +1217,17 @@ void WebRenderBridgeParent::FlushFrameGe
                                               // the root WRBP
 
   // This forces a new GenerateFrame transaction to be sent to the render
   // backend thread, if one is pending. This doesn't block on any other threads.
   if (mCompositorScheduler->NeedsComposite()) {
     mCompositorScheduler->CancelCurrentCompositeTask();
     // Update timestamp of scheduler for APZ and animation.
     mCompositorScheduler->UpdateLastComposeTime();
-    MaybeGenerateFrame(/* aForceGenerateFrame */ true);
+    MaybeGenerateFrame(VsyncId(), /* aForceGenerateFrame */ true);
   }
 }
 
 void WebRenderBridgeParent::FlushFramePresentation() {
   MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread());
 
   // This sends a message to the render backend thread to send a message
   // to the renderer thread, and waits for that message to be processed. So
@@ -1637,21 +1642,22 @@ bool WebRenderBridgeParent::SampleAnimat
   }
 
   return isAnimating;
 }
 
 void WebRenderBridgeParent::CompositeIfNeeded() {
   if (mSkippedComposite) {
     mSkippedComposite = false;
-    CompositeToTarget(nullptr, nullptr);
+    CompositeToTarget(VsyncId(), nullptr, nullptr);
   }
 }
 
-void WebRenderBridgeParent::CompositeToTarget(gfx::DrawTarget* aTarget,
+void WebRenderBridgeParent::CompositeToTarget(VsyncId aId,
+                                              gfx::DrawTarget* aTarget,
                                               const gfx::IntRect* aRect) {
   // This function should only get called in the root WRBP
   MOZ_ASSERT(IsRootWebRenderBridgeParent());
 
   // The two arguments are part of the CompositorVsyncSchedulerOwner API but in
   // this implementation they should never be non-null.
   MOZ_ASSERT(aTarget == nullptr);
   MOZ_ASSERT(aRect == nullptr);
@@ -1672,29 +1678,30 @@ void WebRenderBridgeParent::CompositeToT
     // all pending transactions that have finished scene building.
     for (auto& id : mPendingTransactionIds) {
       if (id.mSceneBuiltTime) {
         id.mSkippedComposites++;
       }
     }
     return;
   }
-  MaybeGenerateFrame(/* aForceGenerateFrame */ false);
+  MaybeGenerateFrame(aId, /* aForceGenerateFrame */ false);
 }
 
 TimeDuration WebRenderBridgeParent::GetVsyncInterval() const {
   // This function should only get called in the root WRBP
   MOZ_ASSERT(IsRootWebRenderBridgeParent());
   if (CompositorBridgeParent* cbp = GetRootCompositorBridgeParent()) {
     return cbp->GetVsyncInterval();
   }
   return TimeDuration();
 }
 
-void WebRenderBridgeParent::MaybeGenerateFrame(bool aForceGenerateFrame) {
+void WebRenderBridgeParent::MaybeGenerateFrame(VsyncId aId,
+                                               bool aForceGenerateFrame) {
   // This function should only get called in the root WRBP
   MOZ_ASSERT(IsRootWebRenderBridgeParent());
 
   TimeStamp start = TimeStamp::Now();
   mAsyncImageManager->SetCompositionTime(start);
 
   // Ensure GenerateFrame is handled on the render backend thread rather
   // than going through the scene builder thread. That way we continue
@@ -1732,37 +1739,37 @@ void WebRenderBridgeParent::MaybeGenerat
     ScheduleGenerateFrame();
   }
   // We do this even if the arrays are empty, because it will clear out any
   // previous properties store on the WR side, which is desirable.
   fastTxn.UpdateDynamicProperties(opacityArray, transformArray);
 
   SetAPZSampleTime();
 
-  wr::RenderThread::Get()->IncPendingFrameCount(mApi->GetId(), start);
+  wr::RenderThread::Get()->IncPendingFrameCount(mApi->GetId(), aId, start);
 
 #if defined(ENABLE_FRAME_LATENCY_LOG)
   auto startTime = TimeStamp::Now();
   mApi->SetFrameStartTime(startTime);
 #endif
 
   fastTxn.GenerateFrame();
 
   mApi->SendTransaction(fastTxn);
 }
 
 void WebRenderBridgeParent::HoldPendingTransactionId(
     const wr::Epoch& aWrEpoch, TransactionId aTransactionId,
-    bool aContainsSVGGroup, const TimeStamp& aRefreshStartTime,
-    const TimeStamp& aTxnStartTime, const nsCString& aTxnURL,
-    const TimeStamp& aFwdTime, const bool aIsFirstPaint,
-    const bool aUseForTelemetry) {
+    bool aContainsSVGGroup, const VsyncId& aVsyncId,
+    const TimeStamp& aRefreshStartTime, const TimeStamp& aTxnStartTime,
+    const nsCString& aTxnURL, const TimeStamp& aFwdTime,
+    const bool aIsFirstPaint, const bool aUseForTelemetry) {
   MOZ_ASSERT(aTransactionId > LastPendingTransactionId());
   mPendingTransactionIds.push_back(PendingTransactionId(
-      aWrEpoch, aTransactionId, aContainsSVGGroup, aRefreshStartTime,
+      aWrEpoch, aTransactionId, aContainsSVGGroup, aVsyncId, aRefreshStartTime,
       aTxnStartTime, aTxnURL, aFwdTime, aIsFirstPaint, aUseForTelemetry));
 }
 
 TransactionId WebRenderBridgeParent::LastPendingTransactionId() {
   TransactionId id{0};
   if (!mPendingTransactionIds.empty()) {
     id = mPendingTransactionIds.back().mId;
   }
@@ -1775,20 +1782,20 @@ void WebRenderBridgeParent::NotifySceneB
     if (id.mEpoch.mHandle == aEpoch.mHandle) {
       id.mSceneBuiltTime = aEndTime;
       break;
     }
   }
 }
 
 TransactionId WebRenderBridgeParent::FlushTransactionIdsForEpoch(
-    const wr::Epoch& aEpoch, const TimeStamp& aCompositeStartTime,
-    const TimeStamp& aRenderStartTime, const TimeStamp& aEndTime,
-    UiCompositorControllerParent* aUiController, wr::RendererStats* aStats,
-    nsTArray<FrameStats>* aOutputStats) {
+    const wr::Epoch& aEpoch, const VsyncId& aCompositeStartId,
+    const TimeStamp& aCompositeStartTime, const TimeStamp& aRenderStartTime,
+    const TimeStamp& aEndTime, UiCompositorControllerParent* aUiController,
+    wr::RendererStats* aStats, nsTArray<FrameStats>* aOutputStats) {
   TransactionId id{0};
   while (!mPendingTransactionIds.empty()) {
     const auto& transactionId = mPendingTransactionIds.front();
 
     if (aEpoch.mHandle < transactionId.mEpoch.mHandle) {
       break;
     }
 
@@ -1815,16 +1822,60 @@ TransactionId WebRenderBridgeParent::Flu
         };
         profiler_add_marker_for_thread(
             profiler_current_thread_id(), "CONTENT_FRAME_TIME",
             MakeUnique<ContentFramePayload>(transactionId.mTxnStartTime,
                                             aEndTime));
       }
 #endif
 
+      // Record CONTENT_FRAME_TIME_REASON.
+      //
+      // This uses the refresh start time (CONTENT_FRAME_TIME uses the start of
+      // display list building), since that includes layout/style time, and 200
+      // should correlate more closely with missing a vsync.
+      //
+      // Also of note is that when the root WebRenderBridgeParent decides to
+      // skip a composite (due to the Renderer being busy), that won't notify
+      // child WebRenderBridgeParents. That failure will show up as the
+      // composite starting late (since it did), but it's really a fault of a
+      // slow composite on the previous frame, not a slow
+      // CONTENT_FULL_PAINT_TIME. It would be nice to have a separate bucket for
+      // this category (scene was ready on the next vsync, but we chose not to
+      // composite), but I can't find a way to locate the right child
+      // WebRenderBridgeParents from the root. WebRender notifies us of the
+      // child pipelines contained within a render, after it finishes, but I
+      // can't see how to query what child pipeline would have been rendered,
+      // when we choose to not do it.
+      latencyMs = (aEndTime - transactionId.mRefreshStartTime).ToMilliseconds();
+      latencyNorm = latencyMs / mVsyncRate.ToMilliseconds();
+      fracLatencyNorm = lround(latencyNorm * 100.0);
+      if (fracLatencyNorm < 200) {
+        // Success
+        Telemetry::AccumulateCategorical(
+            LABELS_CONTENT_FRAME_TIME_REASON::OnTime);
+      } else {
+        if (transactionId.mVsyncId == VsyncId() ||
+            aCompositeStartId == VsyncId() ||
+            transactionId.mVsyncId >= aCompositeStartId) {
+          // Vsync ids are nonsensical, possibly something got trigged from
+          // outside vsync?
+          Telemetry::AccumulateCategorical(
+              LABELS_CONTENT_FRAME_TIME_REASON::NoVsync);
+        } else if (aCompositeStartId - transactionId.mVsyncId > 1) {
+          // Composite started late (and maybe took too long as well)
+          Telemetry::AccumulateCategorical(
+              LABELS_CONTENT_FRAME_TIME_REASON::MissedComposite);
+        } else {
+          // Composite start on time, but must have taken too long.
+          Telemetry::AccumulateCategorical(
+              LABELS_CONTENT_FRAME_TIME_REASON::SlowComposite);
+        }
+      }
+
       if (fracLatencyNorm > 200) {
         aOutputStats->AppendElement(FrameStats(
             transactionId.mId, aCompositeStartTime, aRenderStartTime, aEndTime,
             fracLatencyNorm,
             aStats ? (double(aStats->resource_upload_time) / 1000000.0) : 0.0,
             aStats ? (double(aStats->gpu_cache_upload_time) / 1000000.0) : 0.0,
             transactionId.mTxnStartTime, transactionId.mRefreshStartTime,
             transactionId.mFwdTime, transactionId.mSceneBuiltTime,
--- a/gfx/layers/wr/WebRenderBridgeParent.h
+++ b/gfx/layers/wr/WebRenderBridgeParent.h
@@ -97,30 +97,31 @@ class WebRenderBridgeParent final : publ
       InfallibleTArray<OpDestroy>&& aToDestroy,
       const uint64_t& aFwdTransactionId, const TransactionId& aTransactionId,
       const wr::LayoutSize& aContentSize, ipc::ByteBuf&& dl,
       const wr::BuiltDisplayListDescriptor& dlDesc,
       const WebRenderScrollData& aScrollData,
       nsTArray<OpUpdateResource>&& aResourceUpdates,
       nsTArray<RefCountedShmem>&& aSmallShmems,
       nsTArray<ipc::Shmem>&& aLargeShmems, const wr::IdNamespace& aIdNamespace,
-      const bool& aContainsSVGGroup, const TimeStamp& aRefreshStartTime,
-      const TimeStamp& aTxnStartTime, const nsCString& aTxnURL,
-      const TimeStamp& aFwdTime) override;
+      const bool& aContainsSVGGroup, const VsyncId& aVsyncId,
+      const TimeStamp& aRefreshStartTime, const TimeStamp& aTxnStartTime,
+      const nsCString& aTxnURL, const TimeStamp& aFwdTime) override;
   mozilla::ipc::IPCResult RecvEmptyTransaction(
       const FocusTarget& aFocusTarget, const ScrollUpdatesMap& aUpdates,
       const uint32_t& aPaintSequenceNumber,
       InfallibleTArray<WebRenderParentCommand>&& aCommands,
       InfallibleTArray<OpDestroy>&& aToDestroy,
       const uint64_t& aFwdTransactionId, const TransactionId& aTransactionId,
       nsTArray<OpUpdateResource>&& aResourceUpdates,
       nsTArray<RefCountedShmem>&& aSmallShmems,
       nsTArray<ipc::Shmem>&& aLargeShmems, const wr::IdNamespace& aIdNamespace,
-      const TimeStamp& aRefreshStartTime, const TimeStamp& aTxnStartTime,
-      const nsCString& aTxnURL, const TimeStamp& aFwdTime) override;
+      const VsyncId& aVsyncId, const TimeStamp& aRefreshStartTime,
+      const TimeStamp& aTxnStartTime, const nsCString& aTxnURL,
+      const TimeStamp& aFwdTime) override;
   mozilla::ipc::IPCResult RecvSetFocusTarget(
       const FocusTarget& aFocusTarget) override;
   mozilla::ipc::IPCResult RecvParentCommands(
       nsTArray<WebRenderParentCommand>&& commands) override;
   mozilla::ipc::IPCResult RecvGetSnapshot(PTextureParent* aTexture) override;
 
   mozilla::ipc::IPCResult RecvSetLayersObserverEpoch(
       const LayersObserverEpoch& aChildEpoch) override;
@@ -153,41 +154,41 @@ class WebRenderBridgeParent final : publ
   void Pause();
   bool Resume();
 
   void Destroy();
 
   // CompositorVsyncSchedulerOwner
   bool IsPendingComposite() override { return false; }
   void FinishPendingComposite() override {}
-  void CompositeToTarget(gfx::DrawTarget* aTarget,
+  void CompositeToTarget(VsyncId aId, gfx::DrawTarget* aTarget,
                          const gfx::IntRect* aRect = nullptr) override;
   TimeDuration GetVsyncInterval() const override;
 
   // CompositableParentManager
   bool IsSameProcess() const override;
   base::ProcessId GetChildProcessId() override;
   void NotifyNotUsed(PTextureParent* aTexture,
                      uint64_t aTransactionId) override;
   void SendAsyncMessage(
       const InfallibleTArray<AsyncParentMessageData>& aMessage) override;
   void SendPendingAsyncMessages() override;
   void SetAboutToSendAsyncMessages() override;
 
   void HoldPendingTransactionId(
       const wr::Epoch& aWrEpoch, TransactionId aTransactionId,
-      bool aContainsSVGGroup, const TimeStamp& aRefreshStartTime,
-      const TimeStamp& aTxnStartTime, const nsCString& aTxnURL,
-      const TimeStamp& aFwdTime, const bool aIsFirstPaint,
-      const bool aUseForTelemetry = true);
+      bool aContainsSVGGroup, const VsyncId& aVsyncId,
+      const TimeStamp& aRefreshStartTime, const TimeStamp& aTxnStartTime,
+      const nsCString& aTxnURL, const TimeStamp& aFwdTime,
+      const bool aIsFirstPaint, const bool aUseForTelemetry = true);
   TransactionId LastPendingTransactionId();
   TransactionId FlushTransactionIdsForEpoch(
-      const wr::Epoch& aEpoch, const TimeStamp& aCompositeStartTime,
-      const TimeStamp& aRenderStartTime, const TimeStamp& aEndTime,
-      UiCompositorControllerParent* aUiController,
+      const wr::Epoch& aEpoch, const VsyncId& aCompositeStartId,
+      const TimeStamp& aCompositeStartTime, const TimeStamp& aRenderStartTime,
+      const TimeStamp& aEndTime, UiCompositorControllerParent* aUiController,
       wr::RendererStats* aStats = nullptr,
       nsTArray<FrameStats>* aOutputStats = nullptr);
   void NotifySceneBuiltForEpoch(const wr::Epoch& aEpoch,
                                 const TimeStamp& aEndTime);
 
   void CompositeIfNeeded();
 
   TextureFactoryIdentifier GetTextureFactoryIdentifier();
@@ -310,38 +311,40 @@ class WebRenderBridgeParent final : publ
   // In this case, ScheduleGenerateFrame is not triggered via SceneBuilder.
   // Then we want to rollback WrEpoch. See Bug 1490117.
   void RollbackWrEpoch();
 
   void FlushSceneBuilds();
   void FlushFrameGeneration();
   void FlushFramePresentation();
 
-  void MaybeGenerateFrame(bool aForceGenerateFrame);
+  void MaybeGenerateFrame(VsyncId aId, bool aForceGenerateFrame);
 
  private:
   struct PendingTransactionId {
     PendingTransactionId(const wr::Epoch& aEpoch, TransactionId aId,
-                         bool aContainsSVGGroup,
+                         bool aContainsSVGGroup, const VsyncId& aVsyncId,
                          const TimeStamp& aRefreshStartTime,
                          const TimeStamp& aTxnStartTime,
                          const nsCString& aTxnURL, const TimeStamp& aFwdTime,
                          const bool aIsFirstPaint, const bool aUseForTelemetry)
         : mEpoch(aEpoch),
           mId(aId),
+          mVsyncId(aVsyncId),
           mRefreshStartTime(aRefreshStartTime),
           mTxnStartTime(aTxnStartTime),
           mTxnURL(aTxnURL),
           mFwdTime(aFwdTime),
           mSkippedComposites(0),
           mContainsSVGGroup(aContainsSVGGroup),
           mIsFirstPaint(aIsFirstPaint),
           mUseForTelemetry(aUseForTelemetry) {}
     wr::Epoch mEpoch;
     TransactionId mId;
+    VsyncId mVsyncId;
     TimeStamp mRefreshStartTime;
     TimeStamp mTxnStartTime;
     nsCString mTxnURL;
     TimeStamp mFwdTime;
     TimeStamp mSceneBuiltTime;
     uint32_t mSkippedComposites;
     bool mContainsSVGGroup;
     bool mIsFirstPaint;
--- a/gfx/layers/wr/WebRenderLayerManager.cpp
+++ b/gfx/layers/wr/WebRenderLayerManager.cpp
@@ -227,18 +227,19 @@ bool WebRenderLayerManager::EndEmptyTran
     if (WrBridge()->GetSyncObject() &&
         WrBridge()->GetSyncObject()->IsSyncObjectValid()) {
       WrBridge()->GetSyncObject()->Synchronize();
     }
   }
 
   WrBridge()->EndEmptyTransaction(mFocusTarget, mPendingScrollUpdates,
                                   mAsyncResourceUpdates, mPaintSequenceNumber,
-                                  mLatestTransactionId, refreshStart,
-                                  mTransactionStart, mURL);
+                                  mLatestTransactionId,
+                                  mTransactionIdAllocator->GetVsyncId(),
+                                  refreshStart, mTransactionStart, mURL);
   ClearPendingScrollInfoUpdate();
 
   mTransactionStart = TimeStamp();
 
   MakeSnapshotIfRequired(size);
   return true;
 }
 
@@ -358,18 +359,19 @@ void WebRenderLayerManager::EndTransacti
   wr::BuiltDisplayList dl;
   builder.Finalize(contentSize, dl);
   mLastDisplayListSize = dl.dl.inner.capacity;
 
   {
     AUTO_PROFILER_TRACING("Paint", "ForwardDPTransaction");
     WrBridge()->EndTransaction(contentSize, dl, resourceUpdates,
                                size.ToUnknownSize(), mLatestTransactionId,
-                               mScrollData, containsSVGGroup, refreshStart,
-                               mTransactionStart, mURL);
+                               mScrollData, containsSVGGroup,
+                               mTransactionIdAllocator->GetVsyncId(),
+                               refreshStart, mTransactionStart, mURL);
   }
 
   mTransactionStart = TimeStamp();
 
   MakeSnapshotIfRequired(size);
   mNeedsComposite = false;
 }
 
--- a/gfx/tests/gtest/TestVsync.cpp
+++ b/gfx/tests/gtest/TestVsync.cpp
@@ -25,17 +25,17 @@ using ::testing::_;
 // Windows 8.1 has intermittents at 50 ms. Raise limit to 5 vsync intervals.
 const int kVsyncTimeoutMS = 80;
 
 class TestVsyncObserver : public VsyncObserver {
  public:
   TestVsyncObserver()
       : mDidGetVsyncNotification(false), mVsyncMonitor("VsyncMonitor") {}
 
-  virtual bool NotifyVsync(TimeStamp aVsyncTimeStamp) override {
+  virtual bool NotifyVsync(const VsyncEvent& aVsync) override {
     MonitorAutoLock lock(mVsyncMonitor);
     mDidGetVsyncNotification = true;
     mVsyncMonitor.Notify();
     return true;
   }
 
   void WaitForVsyncNotification() {
     MOZ_ASSERT(NS_IsMainThread());
--- a/gfx/thebes/VsyncSource.cpp
+++ b/gfx/thebes/VsyncSource.cpp
@@ -50,21 +50,24 @@ VsyncSource::Display::~Display() {
   mRefreshTimerVsyncDispatcher = nullptr;
   mCompositorVsyncDispatchers.Clear();
 }
 
 void VsyncSource::Display::NotifyVsync(TimeStamp aVsyncTimestamp) {
   // Called on the vsync thread
   MutexAutoLock lock(mDispatcherLock);
 
+  mVsyncId = mVsyncId.Next();
+  VsyncEvent event(mVsyncId, aVsyncTimestamp);
+
   for (size_t i = 0; i < mCompositorVsyncDispatchers.Length(); i++) {
-    mCompositorVsyncDispatchers[i]->NotifyVsync(aVsyncTimestamp);
+    mCompositorVsyncDispatchers[i]->NotifyVsync(event);
   }
 
-  mRefreshTimerVsyncDispatcher->NotifyVsync(aVsyncTimestamp);
+  mRefreshTimerVsyncDispatcher->NotifyVsync(event);
 }
 
 TimeDuration VsyncSource::Display::GetVsyncRate() {
   // If hardware queries fail / are unsupported, we have to just guess.
   return TimeDuration::FromMilliseconds(1000.0 / 60.0);
 }
 
 void VsyncSource::Display::AddCompositorVsyncDispatcher(
--- a/gfx/thebes/VsyncSource.h
+++ b/gfx/thebes/VsyncSource.h
@@ -6,22 +6,31 @@
 #ifndef GFX_VSYNCSOURCE_H
 #define GFX_VSYNCSOURCE_H
 
 #include "nsTArray.h"
 #include "mozilla/RefPtr.h"
 #include "mozilla/Mutex.h"
 #include "mozilla/TimeStamp.h"
 #include "nsISupportsImpl.h"
+#include "mozilla/layers/LayersTypes.h"
 
 namespace mozilla {
 class RefreshTimerVsyncDispatcher;
 class CompositorVsyncDispatcher;
 
+class VsyncIdType {};
+typedef layers::BaseTransactionId<VsyncIdType> VsyncId;
+
+namespace layout {
+class PVsyncChild;
+}
+
 namespace gfx {
+class PVsyncBridgeParent;
 
 // Controls how and when to enable/disable vsync. Lives as long as the
 // gfxPlatform does on the parent process
 class VsyncSource {
   NS_INLINE_DECL_THREADSAFE_REFCOUNTING(VsyncSource)
 
   typedef mozilla::RefreshTimerVsyncDispatcher RefreshTimerVsyncDispatcher;
   typedef mozilla::CompositorVsyncDispatcher CompositorVsyncDispatcher;
@@ -61,27 +70,49 @@ class VsyncSource {
 
    private:
     void UpdateVsyncStatus();
 
     Mutex mDispatcherLock;
     bool mRefreshTimerNeedsVsync;
     nsTArray<RefPtr<CompositorVsyncDispatcher>> mCompositorVsyncDispatchers;
     RefPtr<RefreshTimerVsyncDispatcher> mRefreshTimerVsyncDispatcher;
+    VsyncId mVsyncId;
   };
 
   void AddCompositorVsyncDispatcher(
       CompositorVsyncDispatcher* aCompositorVsyncDispatcher);
   void RemoveCompositorVsyncDispatcher(
       CompositorVsyncDispatcher* aCompositorVsyncDispatcher);
 
   RefPtr<RefreshTimerVsyncDispatcher> GetRefreshTimerVsyncDispatcher();
   virtual Display& GetGlobalDisplay() = 0;  // Works across all displays
   void Shutdown();
 
  protected:
   virtual ~VsyncSource() {}
 };
 
 }  // namespace gfx
+
+namespace recordreplay {
+namespace child {
+void NotifyVsyncObserver();
+}
+}  // namespace recordreplay
+
+struct VsyncEvent {
+  VsyncId mId;
+  TimeStamp mTime;
+
+ private:
+  VsyncEvent(const VsyncId& aId, const TimeStamp& aTime)
+      : mId(aId), mTime(aTime) {}
+  VsyncEvent() {}
+  friend class gfx::VsyncSource::Display;
+  friend class gfx::PVsyncBridgeParent;
+  friend class layout::PVsyncChild;
+  friend void recordreplay::child::NotifyVsyncObserver();
+};
+
 }  // namespace mozilla
 
 #endif /* GFX_VSYNCSOURCE_H */
--- a/gfx/thebes/gfxPlatform.cpp
+++ b/gfx/thebes/gfxPlatform.cpp
@@ -2552,16 +2552,21 @@ void gfxPlatform::InitCompositorAccelera
          gfxPrefs::WebRenderEnabledDoNotUseDirectly();
 }
 
 /*static*/ bool gfxPlatform::WebRenderEnvvarEnabled() {
   const char* env = PR_GetEnv("MOZ_WEBRENDER");
   return (env && *env == '1');
 }
 
+static bool WebRenderEnvvarDisabled() {
+  const char* env = PR_GetEnv("MOZ_WEBRENDER");
+  return (env && *env == '0');
+}
+
 // If the "gfx.webrender.all.qualified" pref is true we want to enable
 // WebRender for qualifying hardware. The Normandy pref rollout code sets
 // default values on rolled out prefs on every startup, but Gfx starts up
 // before Normandy does. So it's too early to observe the WR qualified pref
 // default value changed by Normandy rollout here yet. So we have a shutdown
 // observer to save the default value on shutdown, and read the saved default
 // value here instead, and emulate the behavior of the pref system, with
 // respect to default/user values of the rollout pref.
@@ -2717,17 +2722,17 @@ void gfxPlatform::InitWebRenderConfig() 
       featureWebRender.ForceDisable(FeatureStatus::Blocked,
                                     "Qualified enable blocked", failureId);
     }
   }
 
   // If the user set the pref to force-disable, let's do that. This will
   // override all the other enabling prefs (gfx.webrender.enabled,
   // gfx.webrender.all, and gfx.webrender.all.qualified).
-  if (gfxPrefs::WebRenderForceDisabled()) {
+  if (gfxPrefs::WebRenderForceDisabled() || WebRenderEnvvarDisabled()) {
     featureWebRender.UserDisable(
         "User force-disabled WR",
         NS_LITERAL_CSTRING("FEATURE_FAILURE_USER_FORCE_DISABLED"));
   }
 
   // HW_COMPOSITING being disabled implies interfacing with the GPU might break
   if (!gfxConfig::IsEnabled(Feature::HW_COMPOSITING)) {
     featureWebRender.ForceDisable(
--- a/gfx/webrender_bindings/RenderThread.cpp
+++ b/gfx/webrender_bindings/RenderThread.cpp
@@ -239,30 +239,33 @@ void RenderThread::HandleFrame(wr::Windo
     return;
   }
 
   if (mHandlingDeviceReset) {
     return;
   }
 
   TimeStamp startTime;
+  VsyncId startId;
 
   bool hadSlowFrame;
   {  // scope lock
     MutexAutoLock lock(mFrameCountMapLock);
     auto it = mWindowInfos.find(AsUint64(aWindowId));
     MOZ_ASSERT(it != mWindowInfos.end());
     WindowInfo* info = it->second;
     MOZ_ASSERT(info->mPendingCount > 0);
     startTime = info->mStartTimes.front();
+    startId = info->mStartIds.front();
     hadSlowFrame = info->mHadSlowFrame;
     info->mHadSlowFrame = false;
   }
 
-  UpdateAndRender(aWindowId, startTime, aRender, /* aReadbackSize */ Nothing(),
+  UpdateAndRender(aWindowId, startId, startTime, aRender,
+                  /* aReadbackSize */ Nothing(),
                   /* aReadbackBuffer */ Nothing(), hadSlowFrame);
   FrameRenderingComplete(aWindowId);
 }
 
 void RenderThread::WakeUp(wr::WindowId aWindowId) {
   if (mHasShutdown) {
     return;
   }
@@ -299,48 +302,46 @@ void RenderThread::RunEvent(wr::WindowId
   }
 
   aEvent->Run(*this, aWindowId);
   aEvent = nullptr;
 }
 
 static void NotifyDidRender(layers::CompositorBridgeParent* aBridge,
                             RefPtr<WebRenderPipelineInfo> aInfo,
+                            VsyncId aCompositeStartId,
                             TimeStamp aCompositeStart, TimeStamp aRenderStart,
                             TimeStamp aEnd, bool aRender,
                             RendererStats aStats) {
   if (aRender && aBridge->GetWrBridge()) {
     // We call this here to mimic the behavior in LayerManagerComposite, as to
     // not change what Talos measures. That is, we do not record an empty frame
     // as a frame.
     aBridge->GetWrBridge()->RecordFrame();
   }
 
   auto info = aInfo->Raw();
 
   for (uintptr_t i = 0; i < info.epochs.length; i++) {
-    aBridge->NotifyPipelineRendered(info.epochs.data[i].pipeline_id,
-                                    info.epochs.data[i].epoch, aCompositeStart,
-                                    aRenderStart, aEnd, &aStats);
-  }
-
-  if (aBridge->GetWrBridge()) {
-    aBridge->GetWrBridge()->CompositeIfNeeded();
+    aBridge->NotifyPipelineRendered(
+        info.epochs.data[i].pipeline_id, info.epochs.data[i].epoch,
+        aCompositeStartId, aCompositeStart, aRenderStart, aEnd, &aStats);
   }
 }
 
 static void NotifyDidStartRender(layers::CompositorBridgeParent* aBridge) {
   // Starting a render will change increment mRenderingCount, and potentially
   // change whether we can allow the bridge to intiate another frame.
   if (aBridge->GetWrBridge()) {
     aBridge->GetWrBridge()->CompositeIfNeeded();
   }
 }
 
 void RenderThread::UpdateAndRender(wr::WindowId aWindowId,
+                                   const VsyncId& aStartId,
                                    const TimeStamp& aStartTime, bool aRender,
                                    const Maybe<gfx::IntSize>& aReadbackSize,
                                    const Maybe<Range<uint8_t>>& aReadbackBuffer,
                                    bool aHadSlowFrame) {
   AUTO_PROFILER_TRACING("Paint", "Composite");
   MOZ_ASSERT(IsInRenderThread());
   MOZ_ASSERT(aRender || aReadbackBuffer.isNothing());
 
@@ -369,18 +370,18 @@ void RenderThread::UpdateAndRender(wr::W
   // Check graphics reset status even when rendering is skipped.
   renderer->CheckGraphicsResetStatus();
 
   TimeStamp end = TimeStamp::Now();
   auto info = renderer->FlushPipelineInfo();
 
   layers::CompositorThreadHolder::Loop()->PostTask(
       NewRunnableFunction("NotifyDidRenderRunnable", &NotifyDidRender,
-                          renderer->GetCompositorBridge(), info, aStartTime,
-                          start, end, aRender, stats));
+                          renderer->GetCompositorBridge(), info, aStartId,
+                          aStartTime, start, end, aRender, stats));
 
   if (rendered) {
     // Wait for GPU after posting NotifyDidRender, since the wait is not
     // necessary for the NotifyDidRender.
     // The wait is necessary for Textures recycling of AsyncImagePipelineManager
     // and for avoiding GPU queue is filled with too much tasks.
     // WaitForGPU's implementation is different for each platform.
     renderer->WaitForGPU();
@@ -458,25 +459,27 @@ void RenderThread::SetDestroyed(wr::Wind
   if (it == mWindowInfos.end()) {
     MOZ_ASSERT(false);
     return;
   }
   it->second->mIsDestroyed = true;
 }
 
 void RenderThread::IncPendingFrameCount(wr::WindowId aWindowId,
+                                        const VsyncId& aStartId,
                                         const TimeStamp& aStartTime) {
   MutexAutoLock lock(mFrameCountMapLock);
   auto it = mWindowInfos.find(AsUint64(aWindowId));
   if (it == mWindowInfos.end()) {
     MOZ_ASSERT(false);
     return;
   }
   it->second->mPendingCount++;
   it->second->mStartTimes.push(aStartTime);
+  it->second->mStartIds.push(aStartId);
 }
 
 void RenderThread::DecPendingFrameCount(wr::WindowId aWindowId) {
   MutexAutoLock lock(mFrameCountMapLock);
   auto it = mWindowInfos.find(AsUint64(aWindowId));
   if (it == mWindowInfos.end()) {
     MOZ_ASSERT(false);
     return;
@@ -490,16 +493,17 @@ void RenderThread::DecPendingFrameCount(
   // This function gets called for "nop frames" where nothing was rendered or
   // composited. But we count this time because the non-WR codepath equivalent
   // in CompositorBridgeParent::ComposeToTarget also counts such frames. And
   // anyway this should be relatively infrequent so it shouldn't skew the
   // numbers much.
   mozilla::Telemetry::AccumulateTimeDelta(mozilla::Telemetry::COMPOSITE_TIME,
                                           info->mStartTimes.front());
   info->mStartTimes.pop();
+  info->mStartIds.pop();
 }
 
 void RenderThread::IncRenderingFrameCount(wr::WindowId aWindowId) {
   MutexAutoLock lock(mFrameCountMapLock);
   auto it = mWindowInfos.find(AsUint64(aWindowId));
   if (it == mWindowInfos.end()) {
     MOZ_ASSERT(false);
     return;
@@ -524,16 +528,17 @@ void RenderThread::FrameRenderingComplet
   info->mRenderingCount--;
 
   // The start time is from WebRenderBridgeParent::CompositeToTarget. From that
   // point until now (when the frame is finally pushed to the screen) is
   // equivalent to the COMPOSITE_TIME metric in the non-WR codepath.
   mozilla::Telemetry::AccumulateTimeDelta(mozilla::Telemetry::COMPOSITE_TIME,
                                           info->mStartTimes.front());
   info->mStartTimes.pop();
+  info->mStartIds.pop();
 }
 
 void RenderThread::NotifySlowFrame(wr::WindowId aWindowId) {
   MutexAutoLock lock(mFrameCountMapLock);
   auto it = mWindowInfos.find(AsUint64(aWindowId));
   if (it == mWindowInfos.end()) {
     MOZ_ASSERT(false);
     return;
--- a/gfx/webrender_bindings/RenderThread.h
+++ b/gfx/webrender_bindings/RenderThread.h
@@ -16,16 +16,17 @@
 #include "mozilla/gfx/Point.h"
 #include "mozilla/MozPromise.h"
 #include "mozilla/Mutex.h"
 #include "mozilla/webrender/webrender_ffi.h"
 #include "mozilla/UniquePtr.h"
 #include "mozilla/webrender/WebRenderTypes.h"
 #include "mozilla/layers/SynchronousTask.h"
 #include "GLContext.h"
+#include "mozilla/VsyncDispatcher.h"
 
 #include <list>
 #include <queue>
 #include <unordered_map>
 
 namespace mozilla {
 namespace wr {
 
@@ -163,18 +164,19 @@ class RenderThread final {
   /// Automatically forwarded to the render thread.
   void PipelineSizeChanged(wr::WindowId aWindowId, uint64_t aPipelineId,
                            float aWidth, float aHeight);
 
   /// Automatically forwarded to the render thread.
   void RunEvent(wr::WindowId aWindowId, UniquePtr<RendererEvent> aCallBack);
 
   /// Can only be called from the render thread.
-  void UpdateAndRender(wr::WindowId aWindowId, const TimeStamp& aStartTime,
-                       bool aRender, const Maybe<gfx::IntSize>& aReadbackSize,
+  void UpdateAndRender(wr::WindowId aWindowId, const VsyncId& aStartId,
+                       const TimeStamp& aStartTime, bool aRender,
+                       const Maybe<gfx::IntSize>& aReadbackSize,
                        const Maybe<Range<uint8_t>>& aReadbackBuffer,
                        bool aHadSlowFrame);
 
   void Pause(wr::WindowId aWindowId);
   bool Resume(wr::WindowId aWindowId);
 
   /// Can be called from any thread.
   void RegisterExternalImage(uint64_t aExternalImageId,
@@ -195,17 +197,17 @@ class RenderThread final {
 
   /// Can be called from any thread.
   bool IsDestroyed(wr::WindowId aWindowId);
   /// Can be called from any thread.
   void SetDestroyed(wr::WindowId aWindowId);
   /// Can be called from any thread.
   bool TooManyPendingFrames(wr::WindowId aWindowId);
   /// Can be called from any thread.
-  void IncPendingFrameCount(wr::WindowId aWindowId,
+  void IncPendingFrameCount(wr::WindowId aWindowId, const VsyncId& aStartId,
                             const TimeStamp& aStartTime);
   /// Can be called from any thread.
   void DecPendingFrameCount(wr::WindowId aWindowId);
   /// Can be called from any thread.
   void IncRenderingFrameCount(wr::WindowId aWindowId);
   /// Can be called from any thread.
   void FrameRenderingComplete(wr::WindowId aWindowId);
 
@@ -261,16 +263,17 @@ class RenderThread final {
 
   struct WindowInfo {
     bool mIsDestroyed = false;
     int64_t mPendingCount = 0;
     int64_t mRenderingCount = 0;
     // One entry in this queue for each pending frame, so the length
     // should always equal mPendingCount
     std::queue<TimeStamp> mStartTimes;
+    std::queue<VsyncId> mStartIds;
     bool mHadSlowFrame = false;
   };
 
   Mutex mFrameCountMapLock;
   std::unordered_map<uint64_t, WindowInfo*> mWindowInfos;
 
   Mutex mRenderTextureMapLock;
   std::unordered_map<uint64_t, RefPtr<RenderTextureHost>> mRenderTextures;
--- a/gfx/webrender_bindings/WebRenderAPI.cpp
+++ b/gfx/webrender_bindings/WebRenderAPI.cpp
@@ -367,18 +367,19 @@ void WebRenderAPI::Readback(const TimeSt
                       gfx::IntSize aSize, const Range<uint8_t>& aBuffer)
         : mTask(aTask), mStartTime(aStartTime), mSize(aSize), mBuffer(aBuffer) {
       MOZ_COUNT_CTOR(Readback);
     }
 
     ~Readback() { MOZ_COUNT_DTOR(Readback); }
 
     virtual void Run(RenderThread& aRenderThread, WindowId aWindowId) override {
-      aRenderThread.UpdateAndRender(aWindowId, mStartTime, /* aRender */ true,
-                                    Some(mSize), Some(mBuffer), false);
+      aRenderThread.UpdateAndRender(aWindowId, VsyncId(), mStartTime,
+                                    /* aRender */ true, Some(mSize),
+                                    Some(mBuffer), false);
       layers::AutoCompleteTask complete(mTask);
     }
 
     layers::SynchronousTask* mTask;
     TimeStamp mStartTime;
     gfx::IntSize mSize;
     const Range<uint8_t>& mBuffer;
   };
--- a/gfx/webrender_bindings/revision.txt
+++ b/gfx/webrender_bindings/revision.txt
@@ -1,1 +1,1 @@
-74b9312fc0305ecaf06322b0f2f466da96e0a64c
+34d58890821d2e606285f21cd6f4befd46c83d47
--- a/gfx/wr/webrender/src/display_list_flattener.rs
+++ b/gfx/wr/webrender/src/display_list_flattener.rs
@@ -976,16 +976,20 @@ impl<'a> DisplayListFlattener<'a> {
                 unreachable!("Should have returned in parent method.")
             }
             SpecificDisplayItem::PushShadow(shadow) => {
                 self.push_shadow(shadow, clip_and_scroll);
             }
             SpecificDisplayItem::PopAllShadows => {
                 self.pop_all_shadows();
             }
+            SpecificDisplayItem::PushCacheMarker(_marker) => {
+            }
+            SpecificDisplayItem::PopCacheMarker => {
+            }
         }
         None
     }
 
     // Given a list of clip sources, a positioning node and
     // a parent clip chain, return a new clip chain entry.
     // If the supplied list of clip sources is empty, then
     // just return the parent clip chain id directly.
--- a/gfx/wr/webrender/src/render_backend.rs
+++ b/gfx/wr/webrender/src/render_backend.rs
@@ -1502,16 +1502,18 @@ impl ToDebugString for SpecificDisplayIt
             SpecificDisplayItem::PushStackingContext(..) => String::from("push_stacking_context"),
             SpecificDisplayItem::RadialGradient(..) => String::from("radial_gradient"),
             SpecificDisplayItem::Rectangle(..) => String::from("rectangle"),
             SpecificDisplayItem::ScrollFrame(..) => String::from("scroll_frame"),
             SpecificDisplayItem::SetGradientStops => String::from("set_gradient_stops"),
             SpecificDisplayItem::StickyFrame(..) => String::from("sticky_frame"),
             SpecificDisplayItem::Text(..) => String::from("text"),
             SpecificDisplayItem::YuvImage(..) => String::from("yuv_image"),
+            SpecificDisplayItem::PushCacheMarker(..) => String::from("push_cache_marker"),
+            SpecificDisplayItem::PopCacheMarker => String::from("pop_cache_marker"),
         }
     }
 }
 
 impl RenderBackend {
     #[cfg(feature = "capture")]
     // Note: the mutable `self` is only needed here for resolving blob images
     fn save_capture(
--- a/gfx/wr/webrender_api/src/display_item.rs
+++ b/gfx/wr/webrender_api/src/display_item.rs
@@ -123,16 +123,18 @@ pub enum SpecificDisplayItem {
     Iframe(IframeDisplayItem),
     PushStackingContext(PushStackingContextDisplayItem),
     PopStackingContext,
     PushReferenceFrame(PushReferenceFrameDisplayListItem),
     PopReferenceFrame,
     SetGradientStops,
     PushShadow(Shadow),
     PopAllShadows,
+    PushCacheMarker(CacheMarkerDisplayItem),
+    PopCacheMarker,
 }
 
 /// This is a "complete" version of the DI specifics,
 /// containing the auxiliary data within the corresponding
 /// enumeration variants, to be used for debug serialization.
 #[cfg(any(feature = "serialize", feature = "deserialize"))]
 #[cfg_attr(feature = "serialize", derive(Serialize))]
 #[cfg_attr(feature = "deserialize", derive(Deserialize))]
@@ -154,16 +156,18 @@ pub enum CompletelySpecificDisplayItem {
     Iframe(IframeDisplayItem),
     PushStackingContext(PushStackingContextDisplayItem, Vec<FilterOp>),
     PopStackingContext,
     PushReferenceFrame(PushReferenceFrameDisplayListItem),
     PopReferenceFrame,
     SetGradientStops(Vec<GradientStop>),
     PushShadow(Shadow),
     PopAllShadows,
+    PushCacheMarker(CacheMarkerDisplayItem),
+    PopCacheMarker,
 }
 
 #[derive(Clone, Copy, Debug, Deserialize, PartialEq, Serialize)]
 pub struct ClipDisplayItem {
     pub id: ClipId,
     pub image_mask: Option<ImageMask>,
 }
 
@@ -512,16 +516,22 @@ pub struct RadialGradientDisplayItem {
     pub tile_spacing: LayoutSize,
 }
 
 #[derive(Clone, Copy, Debug, Deserialize, PartialEq, Serialize)]
 pub struct PushReferenceFrameDisplayListItem {
     pub reference_frame: ReferenceFrame,
 }
 
+/// Provides a hint to WR that it should try to cache the items
+/// within a cache marker context in an off-screen surface.
+#[derive(Clone, Copy, Debug, Deserialize, PartialEq, Serialize)]
+pub struct CacheMarkerDisplayItem {
+}
+
 #[derive(Clone, Copy, Debug, Deserialize, PartialEq, Serialize)]
 pub struct ReferenceFrame {
     pub transform: Option<PropertyBinding<LayoutTransform>>,
     pub perspective: Option<LayoutTransform>,
     pub id: ClipId,
 }
 
 #[derive(Clone, Copy, Debug, Deserialize, PartialEq, Serialize)]
--- a/gfx/wr/webrender_api/src/display_list.rs
+++ b/gfx/wr/webrender_api/src/display_list.rs
@@ -9,17 +9,17 @@ use serde::de::Deserializer;
 #[cfg(feature = "serialize")]
 use serde::ser::{Serializer, SerializeSeq};
 use serde::{Deserialize, Serialize};
 use std::io::{Read, stdout, Write};
 use std::marker::PhantomData;
 use std::ops::Range;
 use std::{io, mem, ptr, slice};
 use time::precise_time_ns;
-use {AlphaType, BorderDetails, BorderDisplayItem, BorderRadius, BoxShadowClipMode};
+use {AlphaType, BorderDetails, BorderDisplayItem, BorderRadius, BoxShadowClipMode, CacheMarkerDisplayItem};
 use {BoxShadowDisplayItem, ClipAndScrollInfo, ClipChainId, ClipChainItem, ClipDisplayItem, ClipId};
 use {ColorF, ComplexClipRegion, DisplayItem, ExtendMode, ExternalScrollId, FilterOp};
 use {FontInstanceKey, GlyphInstance, GlyphOptions, RasterSpace, Gradient, GradientBuilder};
 use {GradientDisplayItem, GradientStop, IframeDisplayItem, ImageDisplayItem, ImageKey, ImageMask};
 use {ImageRendering, LayoutPoint, LayoutPrimitiveInfo, LayoutRect, LayoutSideOffsets, LayoutSize};
 use {LayoutTransform, LayoutVector2D, LineDisplayItem, LineOrientation, LineStyle, MixBlendMode};
 use {PipelineId, PropertyBinding, PushReferenceFrameDisplayListItem};
 use {PushStackingContextDisplayItem, RadialGradient, RadialGradientDisplayItem};
@@ -497,16 +497,18 @@ impl Serialize for BuiltDisplayList {
                     SpecificDisplayItem::PopStackingContext => PopStackingContext,
                     SpecificDisplayItem::PushReferenceFrame(v) => PushReferenceFrame(v),
                     SpecificDisplayItem::PopReferenceFrame => PopReferenceFrame,
                     SpecificDisplayItem::SetGradientStops => SetGradientStops(
                         item.iter.list.get(item.iter.cur_stops).collect()
                     ),
                     SpecificDisplayItem::PushShadow(v) => PushShadow(v),
                     SpecificDisplayItem::PopAllShadows => PopAllShadows,
+                    SpecificDisplayItem::PushCacheMarker(m) => PushCacheMarker(m),
+                    SpecificDisplayItem::PopCacheMarker => PopCacheMarker,
                 },
                 clip_and_scroll: display_item.clip_and_scroll,
                 info: display_item.info,
             };
             seq.serialize_element(&serial_di)?
         }
         seq.end()
     }
@@ -583,16 +585,18 @@ impl<'de> Deserialize<'de> for BuiltDisp
                     }
                     PopReferenceFrame => SpecificDisplayItem::PopReferenceFrame,
                     SetGradientStops(stops) => {
                         DisplayListBuilder::push_iter_impl(&mut temp, stops);
                         SpecificDisplayItem::SetGradientStops
                     },
                     PushShadow(specific_item) => SpecificDisplayItem::PushShadow(specific_item),
                     PopAllShadows => SpecificDisplayItem::PopAllShadows,
+                    PushCacheMarker(marker) => SpecificDisplayItem::PushCacheMarker(marker),
+                    PopCacheMarker => SpecificDisplayItem::PopCacheMarker,
                 },
                 clip_and_scroll: complete.clip_and_scroll,
                 info: complete.info,
             };
             serialize_fast(&mut data, &item);
             // the aux data is serialized after the item, hence the temporary
             data.extend(temp.drain(..));
         }
@@ -1256,16 +1260,29 @@ impl DisplayListBuilder {
                 perspective,
                 id,
             },
         });
         self.push_item(&item, info);
         id
     }
 
+    pub fn push_cache_marker(&mut self) {
+        self.push_new_empty_item(&SpecificDisplayItem::PushCacheMarker(CacheMarkerDisplayItem {
+            // The display item itself is empty for now while we experiment with
+            // the API. In future it may contain extra information, such as details
+            // on whether the surface is known to be opaque and/or a background color
+            // hint that WR should clear the surface to.
+        }));
+    }
+
+    pub fn pop_cache_marker(&mut self) {
+        self.push_new_empty_item(&SpecificDisplayItem::PopCacheMarker);
+    }
+
     pub fn pop_reference_frame(&mut self) {
         self.push_new_empty_item(&SpecificDisplayItem::PopReferenceFrame);
     }
 
     pub fn push_stacking_context(
         &mut self,
         info: &LayoutPrimitiveInfo,
         clip_node_id: Option<ClipId>,
--- a/gfx/wr/wrench/src/yaml_frame_writer.rs
+++ b/gfx/wr/wrench/src/yaml_frame_writer.rs
@@ -1146,16 +1146,21 @@ impl YamlFrameWriter {
 
                     let applied = vec![
                         Yaml::Real(item.previously_applied_offset.x.to_string()),
                         Yaml::Real(item.previously_applied_offset.y.to_string()),
                     ];
                     yaml_node(&mut v, "previously-applied-offset", Yaml::Array(applied));
                 }
 
+                PopCacheMarker => return,
+                PushCacheMarker(_) => {
+                    str_node(&mut v, "type", "cache-marker");
+                }
+
                 PopStackingContext => return,
                 PopReferenceFrame => return,
                 SetGradientStops => panic!("dummy item yielded?"),
                 PushShadow(shadow) => {
                     str_node(&mut v, "type", "shadow");
                     vector_node(&mut v, "offset", &shadow.offset);
                     color_node(&mut v, "color", shadow.color);
                     f32_node(&mut v, "blur-radius", shadow.blur_radius);
--- a/js/src/vm/TraceLogging.cpp
+++ b/js/src/vm/TraceLogging.cpp
@@ -455,24 +455,28 @@ TraceLoggerEventPayload* TraceLoggerThre
     }
     if (!dictionaryData.append(std::move(str))) {
       return nullptr;
     }
 
     nextDictionaryId++;
   }
 
-  uint32_t textId = nextTextId;
+  // Look for a free entry, as some textId's may
+  // already be taken from previous profiling sessions.
+  while (textIdPayloads.has(nextTextId)) {
+    nextTextId++;
+  }
 
-  auto* payload = js_new<TraceLoggerEventPayload>(textId, dictId);
+  auto* payload = js_new<TraceLoggerEventPayload>(nextTextId, dictId);
   if (!payload) {
     return nullptr;
   }
 
-  if (!textIdPayloads.putNew(textId, payload)) {
+  if (!textIdPayloads.putNew(nextTextId, payload)) {
     js_delete(payload);
     return nullptr;
   }
 
   payload->use();
 
   nextTextId++;
 
@@ -971,43 +975,82 @@ void TraceLoggerThread::log(uint32_t id)
 
   mozilla::TimeStamp time = mozilla::TimeStamp::Now();
 
   EventEntry& entry = events.pushUninitialized();
   entry.time = time;
   entry.textId = id;
 }
 
-void TraceLoggerThreadState::clear() {
-  LockGuard<Mutex> guard(lock);
-  for (TraceLoggerThread* logger : threadLoggers) {
-    logger->clear();
-  }
+bool TraceLoggerThreadState::remapDictionaryEntries(
+    mozilla::Vector<UniqueChars, 0, SystemAllocPolicy>* newDictionary,
+    uint32_t* newNextDictionaryId) {
+  MOZ_ASSERT(newNextDictionaryId != nullptr && newDictionary != nullptr);
+
+  typedef HashMap<uint32_t, uint32_t, DefaultHasher<uint32_t>,
+                  SystemAllocPolicy>
+      DictionaryMap;
+  DictionaryMap dictionaryMap;
 
   // Clear all payloads that are not currently used.  There may be some events
-  // that still hold a pointer to a payload.  Restarting the profiler may add
-  // this event to the new events array and so we need to maintain it's
-  // existence.
+  // that still hold a pointer to a payload.  Restarting the profiler may reuse
+  // the exact same event as a previous session if it's still alive so we need
+  // to maintain it's existence.
   for (TextIdToPayloadMap::Enum e(textIdPayloads); !e.empty(); e.popFront()) {
     if (e.front().value()->uses() == 0) {
       js_delete(e.front().value());
       e.removeFront();
+    } else {
+      TraceLoggerEventPayload* payload = e.front().value();
+      uint32_t dictId = payload->dictionaryId();
+
+      if (dictionaryMap.has(dictId)) {
+        DictionaryMap::Ptr mapPointer = dictionaryMap.lookup(dictId);
+        MOZ_ASSERT(mapPointer);
+        payload->setDictionaryId(mapPointer->value());
+      } else {
+        if (!newDictionary->append(std::move(dictionaryData[dictId]))) {
+          return false;
+        }
+        payload->setDictionaryId(*newNextDictionaryId);
+
+        if (!dictionaryMap.putNew(dictId, *newNextDictionaryId)) {
+          return false;
+        }
+
+        (*newNextDictionaryId)++;
+      }
     }
   }
 
-  // Clear and free any data used for the string dictionary.
-  for (auto range = dictionaryData.all(); !range.empty(); range.popFront()) {
-    range.front().reset();
+  return true;
+}
+
+void TraceLoggerThreadState::clear() {
+  LockGuard<Mutex> guard(lock);
+
+  uint32_t newNextDictionaryId = 0;
+  mozilla::Vector<UniqueChars, 0, SystemAllocPolicy> newDictionary;
+  if (remapDictionaryEntries(&newDictionary, &newNextDictionaryId)) {
+    // Clear and free any data used for the string dictionary.
+    for (auto range = dictionaryData.all(); !range.empty(); range.popFront()) {
+      range.front().reset();
+    }
+    dictionaryData.clearAndFree();
+    dictionaryData = std::move(newDictionary);
+
+    payloadDictionary.clearAndCompact();
+
+    nextTextId = TraceLogger_Last;
+    nextDictionaryId = newNextDictionaryId;
   }
 
-  dictionaryData.clearAndFree();
-  payloadDictionary.clearAndCompact();
-
-  nextTextId = TraceLogger_Last;
-  nextDictionaryId = 0;
+  for (TraceLoggerThread* logger : threadLoggers) {
+    logger->clear();
+  }
 }
 
 void TraceLoggerThread::clear() {
   if (graph.get()) {
     graph.reset();
   }
 
   graph = nullptr;
--- a/js/src/vm/TraceLogging.h
+++ b/js/src/vm/TraceLogging.h
@@ -195,16 +195,17 @@ class TraceLoggerEventPayload {
 
   void setLine(uint32_t line) { line_ = mozilla::Some(line); }
   void setColumn(uint32_t col) { col_ = mozilla::Some(col); }
 
   mozilla::Maybe<uint32_t> line() { return line_; }
   mozilla::Maybe<uint32_t> column() { return col_; }
   uint32_t textId() { return textId_; }
   uint32_t dictionaryId() { return dictionaryId_; }
+  void setDictionaryId(uint32_t dictId) { dictionaryId_ = dictId; }
   uint32_t uses() { return uses_; }
 
   // Payloads may have their use count change at any time, *except* the count
   // can only go from zero to non-zero while the thread state lock is held.
   // This should only happen under getOrCreateEventPayload below, and avoids
   // races with purgeUnusedPayloads.
   void use() {
     MOZ_ASSERT_IF(!uses_, CurrentThreadOwnsTraceLoggerThreadStateLock());
@@ -421,16 +422,19 @@ class TraceLoggerThreadState {
   bool init();
   ~TraceLoggerThreadState();
 
   void enableDefaultLogging();
   void enableIonLogging();
   void enableFrontendLogging();
 
   void clear();
+  bool remapDictionaryEntries(
+      mozilla::Vector<UniqueChars, 0, SystemAllocPolicy>* newDictionary,
+      uint32_t* newNextDictionaryId);
 
   TraceLoggerThread* forCurrentThread(JSContext* cx);
   void destroyLogger(TraceLoggerThread* logger);
 
   bool isTextIdEnabled(uint32_t textId) {
     if (textId < TraceLogger_Last) {
       return enabledTextIds[textId];
     }
--- a/layout/base/nsRefreshDriver.cpp
+++ b/layout/base/nsRefreshDriver.cpp
@@ -280,56 +280,56 @@ class RefreshDriverTimer {
   }
 
   /*
    * Actually runs a tick, poking all the attached RefreshDrivers.
    * Grabs the "now" time via TimeStamp::Now().
    */
   void Tick() {
     TimeStamp now = TimeStamp::Now();
-    Tick(now);
+    Tick(VsyncId(), now);
   }
 
-  void TickRefreshDrivers(TimeStamp aNow,
+  void TickRefreshDrivers(VsyncId aId, TimeStamp aNow,
                           nsTArray<RefPtr<nsRefreshDriver>>& aDrivers) {
     if (aDrivers.IsEmpty()) {
       return;
     }
 
     nsTArray<RefPtr<nsRefreshDriver>> drivers(aDrivers);
     for (nsRefreshDriver* driver : drivers) {
       // don't poke this driver if it's in test mode
       if (driver->IsTestControllingRefreshesEnabled()) {
         continue;
       }
 
-      TickDriver(driver, aNow);
+      TickDriver(driver, aId, aNow);
     }
   }
 
   /*
    * Tick the refresh drivers based on the given timestamp.
    */
-  void Tick(TimeStamp now) {
+  void Tick(VsyncId aId, TimeStamp now) {
     ScheduleNextTick(now);
 
     mLastFireTime = now;
 
     LOG("[%p] ticking drivers...", this);
     // RD is short for RefreshDriver
     AUTO_PROFILER_TRACING("Paint", "RefreshDriverTick");
 
-    TickRefreshDrivers(now, mContentRefreshDrivers);
-    TickRefreshDrivers(now, mRootRefreshDrivers);
+    TickRefreshDrivers(aId, now, mContentRefreshDrivers);
+    TickRefreshDrivers(aId, now, mRootRefreshDrivers);
 
     LOG("[%p] done.", this);
   }
 
-  static void TickDriver(nsRefreshDriver* driver, TimeStamp now) {
-    driver->Tick(now);
+  static void TickDriver(nsRefreshDriver* driver, VsyncId aId, TimeStamp now) {
+    driver->Tick(aId, now);
   }
 
   TimeStamp mLastFireTime;
   TimeStamp mTargetTime;
 
   nsTArray<RefPtr<nsRefreshDriver>> mContentRefreshDrivers;
   nsTArray<RefPtr<nsRefreshDriver>> mRootRefreshDrivers;
 
@@ -457,94 +457,98 @@ class VsyncRefreshDriverTimer : public R
           mProcessedVsync(true) {
       MOZ_ASSERT(NS_IsMainThread());
     }
 
     class ParentProcessVsyncNotifier final : public Runnable,
                                              public nsIRunnablePriority {
      public:
       ParentProcessVsyncNotifier(RefreshDriverVsyncObserver* aObserver,
-                                 TimeStamp aVsyncTimestamp)
+                                 VsyncId aId, TimeStamp aVsyncTimestamp)
           : Runnable(
                 "VsyncRefreshDriverTimer::RefreshDriverVsyncObserver::"
                 "ParentProcessVsyncNotifier"),
             mObserver(aObserver),
+            mId(aId),
             mVsyncTimestamp(aVsyncTimestamp) {}
 
       NS_DECL_ISUPPORTS_INHERITED
 
       NS_IMETHOD Run() override {
         MOZ_ASSERT(NS_IsMainThread());
         static bool sCacheInitialized = false;
         static bool sHighPriorityPrefValue = false;
         if (!sCacheInitialized) {
           sCacheInitialized = true;
           Preferences::AddBoolVarCache(&sHighPriorityPrefValue,
                                        "vsync.parentProcess.highPriority",
                                        mozilla::BrowserTabsRemoteAutostart());
         }
         sHighPriorityEnabled = sHighPriorityPrefValue;
 
-        mObserver->TickRefreshDriver(mVsyncTimestamp);
+        mObserver->TickRefreshDriver(mId, mVsyncTimestamp);
         return NS_OK;
       }
 
       NS_IMETHOD GetPriority(uint32_t* aPriority) override {
         *aPriority = sHighPriorityEnabled
                          ? nsIRunnablePriority::PRIORITY_HIGH
                          : nsIRunnablePriority::PRIORITY_NORMAL;
         return NS_OK;
       }
 
      private:
       ~ParentProcessVsyncNotifier() {}
       RefPtr<RefreshDriverVsyncObserver> mObserver;
+      VsyncId mId;
       TimeStamp mVsyncTimestamp;
       static mozilla::Atomic<bool> sHighPriorityEnabled;
     };
 
-    bool NotifyVsync(TimeStamp aVsyncTimestamp) override {
+    bool NotifyVsync(const VsyncEvent& aVsync) override {
       // IMPORTANT: All paths through this method MUST hold a strong ref on
       // |this| for the duration of the TickRefreshDriver callback.
 
       if (!NS_IsMainThread()) {
         MOZ_ASSERT(XRE_IsParentProcess());
         // Compress vsync notifications such that only 1 may run at a time
         // This is so that we don't flood the refresh driver with vsync messages
         // if the main thread is blocked for long periods of time
         {  // scope lock
           MonitorAutoLock lock(mRefreshTickLock);
-          mRecentVsync = aVsyncTimestamp;
+          mRecentVsync = aVsync.mTime;
+          mRecentVsyncId = aVsync.mId;
           if (!mProcessedVsync) {
             return true;
           }
           mProcessedVsync = false;
         }
 
         nsCOMPtr<nsIRunnable> vsyncEvent =
-            new ParentProcessVsyncNotifier(this, aVsyncTimestamp);
+            new ParentProcessVsyncNotifier(this, aVsync.mId, aVsync.mTime);
         NS_DispatchToMainThread(vsyncEvent);
       } else {
-        mRecentVsync = aVsyncTimestamp;
-        if (!mBlockUntil.IsNull() && mBlockUntil > aVsyncTimestamp) {
+        mRecentVsync = aVsync.mTime;
+        mRecentVsyncId = aVsync.mId;
+        if (!mBlockUntil.IsNull() && mBlockUntil > aVsync.mTime) {
           if (mProcessedVsync) {
             // Re-post vsync update as a normal priority runnable. This way
             // runnables already in normal priority queue get processed.
             mProcessedVsync = false;
             nsCOMPtr<nsIRunnable> vsyncEvent = NewRunnableMethod<>(
                 "RefreshDriverVsyncObserver::NormalPriorityNotify", this,
                 &RefreshDriverVsyncObserver::NormalPriorityNotify);
             NS_DispatchToMainThread(vsyncEvent);
           }
 
           return true;
         }
 
         RefPtr<RefreshDriverVsyncObserver> kungFuDeathGrip(this);
-        TickRefreshDriver(aVsyncTimestamp);
+        TickRefreshDriver(aVsync.mId, aVsync.mTime);
       }
 
       return true;
     }
 
     void Shutdown() {
       MOZ_ASSERT(NS_IsMainThread());
       mVsyncRefreshDriverTimer = nullptr;
@@ -556,17 +560,17 @@ class VsyncRefreshDriverTimer : public R
       }
     }
 
     void NormalPriorityNotify() {
       if (mLastProcessedTickInChildProcess.IsNull() ||
           mRecentVsync > mLastProcessedTickInChildProcess) {
         // mBlockUntil is for high priority vsync notifications only.
         mBlockUntil = TimeStamp();
-        TickRefreshDriver(mRecentVsync);
+        TickRefreshDriver(mRecentVsyncId, mRecentVsync);
       }
 
       mProcessedVsync = true;
     }
 
    private:
     ~RefreshDriverVsyncObserver() = default;
 
@@ -610,17 +614,17 @@ class VsyncRefreshDriverTimer : public R
       uint32_t duration = 1 /* ms */;
       for (size_t i = 0;
            i < mozilla::ArrayLength(sJankLevels) && duration < aJankMS;
            ++i, duration *= 2) {
         sJankLevels[i]++;
       }
     }
 
-    void TickRefreshDriver(TimeStamp aVsyncTimestamp) {
+    void TickRefreshDriver(VsyncId aId, TimeStamp aVsyncTimestamp) {
       MOZ_ASSERT(NS_IsMainThread());
 
       RecordTelemetryProbes(aVsyncTimestamp);
       if (XRE_IsParentProcess()) {
         MonitorAutoLock lock(mRefreshTickLock);
         aVsyncTimestamp = mRecentVsync;
         mProcessedVsync = true;
       } else {
@@ -634,32 +638,33 @@ class VsyncRefreshDriverTimer : public R
           (*&rightnow).UsedCanonicalNow() == aVsyncTimestamp.UsedCanonicalNow(),
           aVsyncTimestamp <= *&rightnow);
 
       // We might have a problem that we call ~VsyncRefreshDriverTimer() before
       // the scheduled TickRefreshDriver() runs. Check mVsyncRefreshDriverTimer
       // before use.
       if (mVsyncRefreshDriverTimer) {
         RefPtr<VsyncRefreshDriverTimer> timer = mVsyncRefreshDriverTimer;
-        timer->RunRefreshDrivers(aVsyncTimestamp);
+        timer->RunRefreshDrivers(aId, aVsyncTimestamp);
         // Note: mVsyncRefreshDriverTimer might be null now.
       }
 
       if (!XRE_IsParentProcess()) {
         TimeDuration tickDuration = TimeStamp::Now() - mLastChildTick;
         mBlockUntil = aVsyncTimestamp + tickDuration;
       }
     }
 
     // VsyncRefreshDriverTimer holds this RefreshDriverVsyncObserver and it will
     // be always available before Shutdown(). We can just use the raw pointer
     // here.
     VsyncRefreshDriverTimer* mVsyncRefreshDriverTimer;
     Monitor mRefreshTickLock;
     TimeStamp mRecentVsync;
+    VsyncId mRecentVsyncId;
     TimeStamp mLastChildTick;
     TimeStamp mLastProcessedTickInChildProcess;
     TimeStamp mBlockUntil;
     TimeDuration mVsyncRate;
     bool mProcessedVsync;
   };  // RefreshDriverVsyncObserver
 
   ~VsyncRefreshDriverTimer() override {
@@ -712,17 +717,19 @@ class VsyncRefreshDriverTimer : public R
     --sActiveVsyncTimers;
   }
 
   void ScheduleNextTick(TimeStamp aNowTime) override {
     // Do nothing since we just wait for the next vsync from
     // RefreshDriverVsyncObserver.
   }
 
-  void RunRefreshDrivers(TimeStamp aTimeStamp) { Tick(aTimeStamp); }
+  void RunRefreshDrivers(VsyncId aId, TimeStamp aTimeStamp) {
+    Tick(aId, aTimeStamp);
+  }
 
   RefPtr<RefreshDriverVsyncObserver> mVsyncObserver;
   // Used for parent process.
   RefPtr<RefreshTimerVsyncDispatcher> mVsyncDispatcher;
   // Used for child process.
   // The mVsyncChild will be always available before VsncChild::ActorDestroy().
   // After ActorDestroy(), StartTimer() and StopTimer() calls will be non-op.
   RefPtr<VsyncChild> mVsyncChild;
@@ -866,17 +873,17 @@ class InactiveRefreshDriverTimer final
     mLastFireTime = now;
 
     nsTArray<RefPtr<nsRefreshDriver>> drivers(mContentRefreshDrivers);
     drivers.AppendElements(mRootRefreshDrivers);
     size_t index = mNextDriverIndex;
 
     if (index < drivers.Length() &&
         !drivers[index]->IsTestControllingRefreshesEnabled()) {
-      TickDriver(drivers[index], now);
+      TickDriver(drivers[index], VsyncId(), now);
     }
 
     mNextDriverIndex++;
   }
 
   static void TimerTickOne(nsITimer* aTimer, void* aClosure) {
     RefPtr<InactiveRefreshDriverTimer> timer =
         static_cast<InactiveRefreshDriverTimer*>(aClosure);
@@ -1359,19 +1366,19 @@ nsRefreshDriver::ObserverArray& nsRefres
 
 void nsRefreshDriver::DoTick() {
   MOZ_ASSERT(!IsFrozen(), "Why are we notified while frozen?");
   MOZ_ASSERT(mPresContext, "Why are we notified after disconnection?");
   MOZ_ASSERT(!nsContentUtils::GetCurrentJSContext(),
              "Shouldn't have a JSContext on the stack");
 
   if (mTestControllingRefreshes) {
-    Tick(mMostRecentRefresh);
+    Tick(VsyncId(), mMostRecentRefresh);
   } else {
-    Tick(TimeStamp::Now());
+    Tick(VsyncId(), TimeStamp::Now());
   }
 }
 
 struct DocumentFrameCallbacks {
   explicit DocumentFrameCallbacks(nsIDocument* aDocument)
       : mDocument(aDocument) {}
 
   nsCOMPtr<nsIDocument> mDocument;
@@ -1603,17 +1610,17 @@ void nsRefreshDriver::CancelIdleRunnable
   }
 
   if (sPendingIdleRunnables->IsEmpty()) {
     delete sPendingIdleRunnables;
     sPendingIdleRunnables = nullptr;
   }
 }
 
-void nsRefreshDriver::Tick(TimeStamp aNowTime) {
+void nsRefreshDriver::Tick(VsyncId aId, TimeStamp aNowTime) {
   MOZ_ASSERT(!nsContentUtils::GetCurrentJSContext(),
              "Shouldn't have a JSContext on the stack");
 
   if (nsNPAPIPluginInstance::InPluginCallUnsafeForReentry()) {
     NS_ERROR("Refresh driver should not run during plugin call!");
     // Try to survive this by just ignoring the refresh tick.
     return;
   }
@@ -1670,16 +1677,17 @@ void nsRefreshDriver::Tick(TimeStamp aNo
 
   mResizeSuppressed = false;
 
   AutoRestore<bool> restoreInRefresh(mInRefresh);
   mInRefresh = true;
 
   AutoRestore<TimeStamp> restoreTickStart(mTickStart);
   mTickStart = TimeStamp::Now();
+  mTickVsyncId = aId;
 
   gfxPlatform::GetPlatform()->SchedulePaintIfDeviceReset();
 
   // We want to process any pending APZ metrics ahead of their positions
   // in the queue. This will prevent us from spending precious time
   // painting a stale displayport.
   if (gfxPrefs::APZPeekMessages()) {
     nsLayoutUtils::UpdateDisplayPortMarginsFromPendingMessages();
@@ -2047,16 +2055,18 @@ void nsRefreshDriver::ClearPendingTransa
 void nsRefreshDriver::ResetInitialTransactionId(
     mozilla::layers::TransactionId aTransactionId) {
   mCompletedTransaction = mOutstandingTransactionId = mNextTransactionId =
       aTransactionId;
 }
 
 mozilla::TimeStamp nsRefreshDriver::GetTransactionStart() { return mTickStart; }
 
+VsyncId nsRefreshDriver::GetVsyncId() { return mTickVsyncId; }
+
 void nsRefreshDriver::NotifyTransactionCompleted(
     mozilla::layers::TransactionId aTransactionId) {
   if (aTransactionId > mCompletedTransaction) {
     if (mOutstandingTransactionId - mCompletedTransaction > 1 &&
         mWaitingForTransaction) {
       mCompletedTransaction = aTransactionId;
       FinishedWaitingForTransaction();
     } else {
--- a/layout/base/nsRefreshDriver.h
+++ b/layout/base/nsRefreshDriver.h
@@ -20,16 +20,17 @@
 #include "nsTObserverArray.h"
 #include "nsTArray.h"
 #include "nsTHashtable.h"
 #include "nsClassHashtable.h"
 #include "nsHashKeys.h"
 #include "mozilla/Attributes.h"
 #include "mozilla/Maybe.h"
 #include "mozilla/layers/TransactionIdAllocator.h"
+#include "mozilla/VsyncDispatcher.h"
 
 class nsPresContext;
 class nsIPresShell;
 class nsIDocument;
 class imgIRequest;
 class nsINode;
 class nsIRunnable;
 
@@ -356,16 +357,17 @@ class nsRefreshDriver final : public moz
   // mozilla::layers::TransactionIdAllocator
   TransactionId GetTransactionId(bool aThrottle) override;
   TransactionId LastTransactionId() const override;
   void NotifyTransactionCompleted(TransactionId aTransactionId) override;
   void RevokeTransactionId(TransactionId aTransactionId) override;
   void ClearPendingTransactions() override;
   void ResetInitialTransactionId(TransactionId aTransactionId) override;
   mozilla::TimeStamp GetTransactionStart() override;
+  mozilla::VsyncId GetVsyncId() override;
 
   bool IsWaitingForPaint(mozilla::TimeStamp aTime);
 
   // nsARefreshObserver
   NS_IMETHOD_(MozExternalRefCountType) AddRef(void) override {
     return TransactionIdAllocator::AddRef();
   }
   NS_IMETHOD_(MozExternalRefCountType) Release(void) override {
@@ -409,17 +411,17 @@ class nsRefreshDriver final : public moz
     RequestTable mEntries;
   };
   typedef nsClassHashtable<nsUint32HashKey, ImageStartData> ImageStartTable;
 
   void RunFullscreenSteps();
   void DispatchAnimationEvents();
   void RunFrameRequestCallbacks(mozilla::TimeStamp aNowTime);
   void UpdateIntersectionObservations();
-  void Tick(mozilla::TimeStamp aNowTime);
+  void Tick(mozilla::VsyncId aId, mozilla::TimeStamp aNowTime);
 
   enum EnsureTimerStartedFlags {
     eNone = 0,
     eForceAdjustTimer = 1 << 0,
     eAllowTimeToGoBackwards = 1 << 1,
     eNeverAdjustTimer = 1 << 2,
   };
   void EnsureTimerStarted(EnsureTimerStartedFlags aFlags = eNone);
@@ -502,16 +504,17 @@ class nsRefreshDriver final : public moz
   bool mNotifyDOMContentFlushed;
 
   // Number of seconds that the refresh driver is blocked waiting for a
   // compositor transaction to be completed before we append a note to the gfx
   // critical log. The number is doubled every time the threshold is hit.
   uint64_t mWarningThreshold;
   mozilla::TimeStamp mMostRecentRefresh;
   mozilla::TimeStamp mTickStart;
+  mozilla::VsyncId mTickVsyncId;
   mozilla::TimeStamp mNextThrottledFrameRequestTick;
   mozilla::TimeStamp mNextRecomputeVisibilityTick;
 
   // separate arrays for each flush type we support
   ObserverArray mObservers[4];
   // These observers should NOT be included in HasObservers() since that method
   // is used to determine whether or not to stop the timer, or restore it when
   // thawing the refresh driver. On the other hand these observers are intended
--- a/layout/ipc/PVsync.ipdl
+++ b/layout/ipc/PVsync.ipdl
@@ -2,32 +2,33 @@
 /* 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 protocol PBackground;
 include "mozilla/layers/LayersMessageUtils.h";
 
 using class mozilla::TimeStamp from "mozilla/TimeStamp.h";
+using mozilla::VsyncEvent from "mozilla/VsyncDispatcher.h";
 
 namespace mozilla {
 namespace layout {
 
 /*
  * The PVsync is a sub-protocol in PBackground and it is used to notify
  * the vsync event from chrome to content process. It also provides the
  * interfaces for content to observe/unobserve vsync event notifications.
  */
 async protocol PVsync
 {
   manager PBackground;
 
 child:
   // Send vsync event from chrome to content process.
-  prio(high) async Notify(TimeStamp aVsyncTimestamp) compress;
+  prio(high) async Notify(VsyncEvent aVsync) compress;
 
   // Send the vsync rate to the content process.
   async VsyncRate(float aVsyncRate);
 
 parent:
   // Content process use these messages to acquire the vsync event.
   async Observe();
   async Unobserve();
--- a/layout/ipc/VsyncChild.cpp
+++ b/layout/ipc/VsyncChild.cpp
@@ -46,29 +46,28 @@ void VsyncChild::ActorDestroy(ActorDestr
   mIsShutdown = true;
   mObserver = nullptr;
 
   if (recordreplay::IsRecordingOrReplaying()) {
     recordreplay::child::SetVsyncObserver(nullptr);
   }
 }
 
-mozilla::ipc::IPCResult VsyncChild::RecvNotify(
-    const TimeStamp& aVsyncTimestamp) {
+mozilla::ipc::IPCResult VsyncChild::RecvNotify(const VsyncEvent& aVsync) {
   MOZ_ASSERT(NS_IsMainThread());
   MOZ_ASSERT(!mIsShutdown);
 
   SchedulerGroup::MarkVsyncRan();
   if (mObservingVsync && mObserver) {
     if (recordreplay::IsRecordingOrReplaying() &&
         !recordreplay::child::OnVsync()) {
       return IPC_OK();
     }
 
-    mObserver->NotifyVsync(aVsyncTimestamp);
+    mObserver->NotifyVsync(aVsync);
   }
   return IPC_OK();
 }
 
 void VsyncChild::SetVsyncObserver(VsyncObserver* aVsyncObserver) {
   MOZ_ASSERT(NS_IsMainThread());
   mObserver = aVsyncObserver;
 
--- a/layout/ipc/VsyncChild.h
+++ b/layout/ipc/VsyncChild.h
@@ -47,18 +47,17 @@ class VsyncChild final : public PVsyncCh
   // TimeDuration::Forever() if mVsyncRate hasn't been set by calling
   // GetVsyncRate.
   TimeDuration VsyncRate();
 
  private:
   VsyncChild();
   virtual ~VsyncChild();
 
-  virtual mozilla::ipc::IPCResult RecvNotify(
-      const TimeStamp& aVsyncTimestamp) override;
+  virtual mozilla::ipc::IPCResult RecvNotify(const VsyncEvent& aVsync) override;
   virtual mozilla::ipc::IPCResult RecvVsyncRate(
       const float& aVsyncRate) override;
   virtual void ActorDestroy(ActorDestroyReason aActorDestroyReason) override;
 
   bool mObservingVsync;
   bool mIsShutdown;
 
   // The content side vsync observer.
--- a/layout/ipc/VsyncParent.cpp
+++ b/layout/ipc/VsyncParent.cpp
@@ -37,37 +37,37 @@ VsyncParent::VsyncParent()
   AssertIsOnBackgroundThread();
 }
 
 VsyncParent::~VsyncParent() {
   // Since we use NS_INLINE_DECL_THREADSAFE_REFCOUNTING, we can't make sure
   // VsyncParent is always released on the background thread.
 }
 
-bool VsyncParent::NotifyVsync(TimeStamp aTimeStamp) {
+bool VsyncParent::NotifyVsync(const VsyncEvent& aVsync) {
   // Called on hardware vsync thread. We should post to current ipc thread.
   MOZ_ASSERT(!IsOnBackgroundThread());
-  nsCOMPtr<nsIRunnable> vsyncEvent = NewRunnableMethod<TimeStamp>(
+  nsCOMPtr<nsIRunnable> vsyncEvent = NewRunnableMethod<VsyncEvent>(
       "layout::VsyncParent::DispatchVsyncEvent", this,
-      &VsyncParent::DispatchVsyncEvent, aTimeStamp);
+      &VsyncParent::DispatchVsyncEvent, aVsync);
   MOZ_ALWAYS_SUCCEEDS(
       mBackgroundThread->Dispatch(vsyncEvent, NS_DISPATCH_NORMAL));
   return true;
 }
 
-void VsyncParent::DispatchVsyncEvent(TimeStamp aTimeStamp) {
+void VsyncParent::DispatchVsyncEvent(const VsyncEvent& aVsync) {
   AssertIsOnBackgroundThread();
 
   // If we call NotifyVsync() when we handle ActorDestroy() message, we might
   // still call DispatchVsyncEvent().
   // Similarly, we might also receive RecvUnobserveVsync() when call
   // NotifyVsync(). We use mObservingVsync and mDestroyed flags to skip this
   // notification.
   if (mObservingVsync && !mDestroyed) {
-    Unused << SendNotify(aTimeStamp);
+    Unused << SendNotify(aVsync);
   }
 }
 
 mozilla::ipc::IPCResult VsyncParent::RecvRequestVsyncRate() {
   AssertIsOnBackgroundThread();
   TimeDuration vsyncRate = gfxPlatform::GetPlatform()
                                ->GetHardwareVsync()
                                ->GetGlobalDisplay()
--- a/layout/ipc/VsyncParent.h
+++ b/layout/ipc/VsyncParent.h
@@ -29,24 +29,24 @@ class VsyncParent final : public PVsyncP
   friend class mozilla::ipc::BackgroundParentImpl;
 
  private:
   static already_AddRefed<VsyncParent> Create();
 
   VsyncParent();
   virtual ~VsyncParent();
 
-  virtual bool NotifyVsync(TimeStamp aTimeStamp) override;
+  virtual bool NotifyVsync(const VsyncEvent& aVsync) override;
   virtual mozilla::ipc::IPCResult RecvRequestVsyncRate() override;
 
   virtual mozilla::ipc::IPCResult RecvObserve() override;
   virtual mozilla::ipc::IPCResult RecvUnobserve() override;
   virtual void ActorDestroy(ActorDestroyReason aActorDestroyReason) override;
 
-  void DispatchVsyncEvent(TimeStamp aTimeStamp);
+  void DispatchVsyncEvent(const VsyncEvent& aVsync);
 
   bool mObservingVsync;
   bool mDestroyed;
   nsCOMPtr<nsIThread> mBackgroundThread;
   RefPtr<RefreshTimerVsyncDispatcher> mVsyncDispatcher;
 };
 
 }  // namespace layout
new file mode 100644
--- /dev/null
+++ b/media/libdav1d/config/other/config.h
@@ -0,0 +1,27 @@
+/*
+ * Autogenerated by the Meson build system.
+ * Do not edit, your changes will be lost.
+ */
+
+#pragma once
+
+#define ARCH_AARCH64 0
+
+#define ARCH_ARM 0
+
+#define ARCH_X86 0
+
+#define ARCH_X86_32 0
+
+#define ARCH_X86_64 0
+
+#define CONFIG_10BPC 1
+
+#define CONFIG_8BPC 1
+
+#define HAVE_ASM 0
+
+#define HAVE_POSIX_MEMALIGN 1
+
+#define HAVE_UNISTD_H 1
+
--- a/media/libdav1d/moz.build
+++ b/media/libdav1d/moz.build
@@ -28,16 +28,19 @@ elif CONFIG['CPU_ARCH'] == 'x86_64':
         LOCAL_INCLUDES += ['/media/libdav1d/config/x86_64/']
         EXPORTS.dav1d += ['config/x86_64/config.h']
 elif CONFIG['CPU_ARCH'] == 'arm':
     LOCAL_INCLUDES += ['/media/libdav1d/config/arm/']
     EXPORTS.dav1d += ['config/arm/config.h']
 elif CONFIG['CPU_ARCH'] == 'aarch64':
     LOCAL_INCLUDES += ['/media/libdav1d/config/aarch64/']
     EXPORTS.dav1d += ['config/aarch64/config.h']
+else:
+    LOCAL_INCLUDES += ['/media/libdav1d/config/other/']
+    EXPORTS.dav1d += ['config/other/config.h']
 
 EXPORTS.dav1d += [
     'version.h',
 ]
 
 # entrypoint source files
 SOURCES += [
     '../../third_party/dav1d/src/lib.c',
--- a/media/webrtc/trunk/webrtc/modules/video_coding/jitter_buffer.cc
+++ b/media/webrtc/trunk/webrtc/modules/video_coding/jitter_buffer.cc
@@ -568,22 +568,16 @@ VCMEncodedFrame* VCMJitterBuffer::Extrac
 
   // We have a frame - update the last decoded state and nack list.
   last_decoded_state_.SetState(frame);
   DropPacketsFromNackList(last_decoded_state_.sequence_num());
 
   if ((*frame).IsSessionComplete())
     UpdateAveragePacketsPerFrame(frame->NumPackets());
 
-  if (frame->Length() == 0) {
-    // Normally only if MakeDecodable() on an incomplete frame threw it all away
-    ReleaseFrame(frame);
-    return NULL;
-  }
-
   return frame;
 }
 
 // Release frame when done with decoding. Should never be used to release
 // frames from within the jitter buffer.
 void VCMJitterBuffer::ReleaseFrame(VCMEncodedFrame* frame) {
   RTC_CHECK(frame != nullptr);
   rtc::CritScope cs(&crit_sect_);
--- a/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/scanners/WifiScanner.java
+++ b/mobile/android/stumbler/java/org/mozilla/mozstumbler/service/stumblerthread/scanners/WifiScanner.java
@@ -52,17 +52,17 @@ public class WifiScanner extends Broadca
 
     /* Testing */
     public static boolean sIsTestMode;
     public List<ScanResult> mTestModeFakeScanResults = new ArrayList<ScanResult>();
     public Set<String> getAccessPoints(android.test.AndroidTestCase restrictedAccessor) { return mAPs; }
     /* ------- */
 
     public WifiScanner(Context c) {
-        mContext = c;
+        mContext = c.getApplicationContext();
     }
 
     private boolean isWifiEnabled() {
         return (sIsTestMode) || getWifiManager().isWifiEnabled();
     }
 
     private List<ScanResult> getScanResults() {
         WifiManager manager = getWifiManager();
--- a/taskcluster/ci/test/awsy.yml
+++ b/taskcluster/ci/test/awsy.yml
@@ -1,15 +1,16 @@
 job-defaults:
     suite: awsy
     max-run-time: 7200
     instance-size: xlarge
     virtualization:
         by-test-platform:
             windows10-64-qr/.*: virtual-with-gpu
+            windows10-64/.*: virtual-with-gpu
             default: virtual
     allow-software-gl-layers: false
     mozharness:
         script: awsy_script.py
         config:
             by-test-platform:
                 windows.*:
                     - awsy/taskcluster_windows_config.py
--- a/taskcluster/taskgraph/transforms/tests.py
+++ b/taskcluster/taskgraph/transforms/tests.py
@@ -950,16 +950,21 @@ def enable_webrender(config, tests):
     """
     Handle the "webrender" property by passing a flag to mozharness if it is
     enabled.
     """
     for test in tests:
         if test.get('webrender'):
             test['mozharness'].setdefault('extra-options', [])\
                               .append("--enable-webrender")
+        # Explicitly disable WebRender on non-WR AWSY, since that job runs on
+        # virtual-with-gpu and thus is considered qualified hardware.
+        elif test['suite'] == 'awsy':
+            test['mozharness'].setdefault('extra-options', [])\
+                              .append("--disable-webrender")
 
         yield test
 
 
 @transforms.add
 def set_retry_exit_status(config, tests):
     """Set the retry exit status to TBPL_RETRY, the value returned by mozharness
        scripts to indicate a transient failure that should be retried."""
--- a/testing/mozharness/scripts/awsy_script.py
+++ b/testing/mozharness/scripts/awsy_script.py
@@ -46,16 +46,22 @@ class AWSY(TestingMixin, MercurialScript
           "help": "Set STYLO_THREADS=1.",
           }],
         [["--enable-webrender"],
          {"action": "store_true",
           "dest": "enable_webrender",
           "default": False,
           "help": "Tries to enable the WebRender compositor.",
           }],
+        [["--disable-webrender"],
+         {"action": "store_true",
+          "dest": "disable_webrender",
+          "default": False,
+          "help": "Force-disables the WebRender compositor.",
+          }],
         [["--base"],
          {"action": "store_true",
           "dest": "test_about_blank",
           "default": False,
           "help": "Runs the about:blank base case memory test.",
           }],
         [["--dmd"],
          {"action": "store_true",
@@ -227,16 +233,21 @@ class AWSY(TestingMixin, MercurialScript
 
         # TODO: consider getting rid of this as stylo is enabled by default
         env['STYLO_FORCE_ENABLED'] = '1'
 
         if self.config['enable_webrender']:
             env['MOZ_WEBRENDER'] = '1'
             env['MOZ_ACCELERATED'] = '1'
 
+        # Allow explicitly disabling webrender, so that we don't run WR on non-QR
+        # test platforms just because they run on qualified hardware.
+        if self.config['disable_webrender']:
+            env['MOZ_WEBRENDER'] = '0'
+
         env['MOZ_UPLOAD_DIR'] = dirs['abs_blob_upload_dir']
         if not os.path.isdir(env['MOZ_UPLOAD_DIR']):
             self.mkdir_p(env['MOZ_UPLOAD_DIR'])
         if self.query_minidump_stackwalk():
             env['MINIDUMP_STACKWALK'] = self.minidump_stackwalk_path
         env['MINIDUMP_SAVE_PATH'] = dirs['abs_blob_upload_dir']
         env['RUST_BACKTRACE'] = '1'
         env = self.query_env(partial_env=env)
--- a/toolkit/components/normandy/content/about-studies/about-studies.css
+++ b/toolkit/components/normandy/content/about-studies/about-studies.css
@@ -135,27 +135,36 @@ button > .button-box {
 
 .study-details {
   flex: 1;
   overflow: hidden;
 }
 
 .study-name {
   font-weight: bold;
-  margin-bottom: 0.3em;
   white-space: pre;
 }
 
+.study-header {
+  margin-bottom: .3em;
+}
+
 .study-header > * {
   margin-right: 5px;
 }
 
 .study-header > *:last-child {
   margin-right: 0;
 }
 
 .study-description code {
-  font: italic 1.0rem 'Fira Mono', 'mono', 'monospace';
+  background-color: var(--grey-20);
+  border-radius: 3px;
+  box-sizing: border-box;
+  color: var(--in-content-text-color);
+  font-size: 85%;
+  font-family: 'Fira Mono', 'mono', monospace;
+  padding: .2em .4em;
 }
 
 .study-actions {
   flex: 0 0;
 }
--- a/toolkit/components/telemetry/Histograms.json
+++ b/toolkit/components/telemetry/Histograms.json
@@ -13206,16 +13206,25 @@
     "alert_emails": ["gfx-telemetry-alerts@mozilla.com", "mwoodrow@mozilla.com"],
     "bug_numbers": [1503405],
     "expires_in_version": "70",
     "kind": "exponential",
     "high": 5000,
     "n_buckets": 50,
     "description": "The time, in percentage of a vsync interval, spent from beginning a paint in the content process until that frame is presented in the compositor by WebRender, excluding time spent uploading any content"
   },
+  "CONTENT_FRAME_TIME_REASON": {
+    "record_in_processes": ["main", "gpu"],
+    "alert_emails": ["gfx-telemetry-alerts@mozilla.com", "mwoodrow@mozilla.com"],
+    "bug_numbers": [1510853],
+    "expires_in_version": "73",
+    "kind": "categorical",
+    "description": "The reason that CONTENT_FRAME_TIME recorded a slow (>200) result, if any.",
+    "labels": ["OnTime", "NoVsync", "MissedComposite", "SlowComposite"]
+  },
   "CONTENT_LARGE_PAINT_PHASE_WEIGHT": {
     "record_in_processes": ["main", "content"],
     "alert_emails": ["gfx-telemetry-alerts@mozilla.com", "mwoodrow@mozilla.com"],
     "bug_numbers": [1309442],
     "expires_in_version": "66",
     "keyed": true,
     "keys": ["dl", "flb", "fr", "r"],
     "kind": "linear",
--- a/toolkit/recordreplay/ipc/ChildIPC.cpp
+++ b/toolkit/recordreplay/ipc/ChildIPC.cpp
@@ -409,19 +409,22 @@ void NotifyAlwaysMarkMajorCheckpoints() 
 
 static VsyncObserver* gVsyncObserver;
 
 void SetVsyncObserver(VsyncObserver* aObserver) {
   MOZ_RELEASE_ASSERT(!gVsyncObserver || !aObserver);
   gVsyncObserver = aObserver;
 }
 
-static void NotifyVsyncObserver() {
+void NotifyVsyncObserver() {
   if (gVsyncObserver) {
-    gVsyncObserver->NotifyVsync(TimeStamp::Now());
+    static VsyncId vsyncId;
+    vsyncId = vsyncId.Next();
+    VsyncEvent event(vsyncId, TimeStamp::Now());
+    gVsyncObserver->NotifyVsync(event);
   }
 }
 
 // How many paints have been started and haven't reached PaintFromMainThread
 // yet. Only accessed on the main thread.
 static int32_t gNumPendingMainThreadPaints;
 
 bool OnVsync() {
--- a/widget/VsyncDispatcher.cpp
+++ b/widget/VsyncDispatcher.cpp
@@ -22,24 +22,23 @@ CompositorVsyncDispatcher::CompositorVsy
 }
 
 CompositorVsyncDispatcher::~CompositorVsyncDispatcher() {
   MOZ_ASSERT(XRE_IsParentProcess());
   // We auto remove this vsync dispatcher from the vsync source in the
   // nsBaseWidget
 }
 
-void CompositorVsyncDispatcher::NotifyVsync(TimeStamp aVsyncTimestamp) {
+void CompositorVsyncDispatcher::NotifyVsync(const VsyncEvent& aVsync) {
   // In vsync thread
-  layers::CompositorBridgeParent::PostInsertVsyncProfilerMarker(
-      aVsyncTimestamp);
+  layers::CompositorBridgeParent::PostInsertVsyncProfilerMarker(aVsync.mTime);
 
   MutexAutoLock lock(mCompositorObserverLock);
   if (mCompositorVsyncObserver) {
-    mCompositorVsyncObserver->NotifyVsync(aVsyncTimestamp);
+    mCompositorVsyncObserver->NotifyVsync(aVsync);
   }
 }
 
 void CompositorVsyncDispatcher::ObserveVsync(bool aEnable) {
   MOZ_ASSERT(NS_IsMainThread());
   MOZ_ASSERT(XRE_IsParentProcess());
   if (mDidShutdown) {
     return;
@@ -96,25 +95,25 @@ RefreshTimerVsyncDispatcher::RefreshTime
   MOZ_ASSERT(NS_IsMainThread());
 }
 
 RefreshTimerVsyncDispatcher::~RefreshTimerVsyncDispatcher() {
   MOZ_ASSERT(XRE_IsParentProcess() || recordreplay::IsRecordingOrReplaying());
   MOZ_ASSERT(NS_IsMainThread());
 }
 
-void RefreshTimerVsyncDispatcher::NotifyVsync(TimeStamp aVsyncTimestamp) {
+void RefreshTimerVsyncDispatcher::NotifyVsync(const VsyncEvent& aVsync) {
   MutexAutoLock lock(mRefreshTimersLock);
 
   for (size_t i = 0; i < mChildRefreshTimers.Length(); i++) {
-    mChildRefreshTimers[i]->NotifyVsync(aVsyncTimestamp);
+    mChildRefreshTimers[i]->NotifyVsync(aVsync);
   }
 
   if (mParentRefreshTimer) {
-    mParentRefreshTimer->NotifyVsync(aVsyncTimestamp);
+    mParentRefreshTimer->NotifyVsync(aVsync);
   }
 }
 
 void RefreshTimerVsyncDispatcher::SetParentRefreshTimer(
     VsyncObserver* aVsyncObserver) {
   MOZ_ASSERT(NS_IsMainThread());
   {  // lock scope because UpdateVsyncStatus runs on main thread and will
      // deadlock
--- a/widget/VsyncDispatcher.h
+++ b/widget/VsyncDispatcher.h
@@ -6,29 +6,30 @@
 #ifndef mozilla_widget_VsyncDispatcher_h
 #define mozilla_widget_VsyncDispatcher_h
 
 #include "mozilla/Mutex.h"
 #include "mozilla/TimeStamp.h"
 #include "nsISupportsImpl.h"
 #include "nsTArray.h"
 #include "mozilla/RefPtr.h"
+#include "VsyncSource.h"
 
 namespace mozilla {
 
 class VsyncObserver {
   NS_INLINE_DECL_THREADSAFE_REFCOUNTING(VsyncObserver)
 
  public:
   // The method called when a vsync occurs. Return true if some work was done.
   // In general, this vsync notification will occur on the hardware vsync
   // thread from VsyncSource. But it might also be called on PVsync ipc thread
   // if this notification is cross process. Thus all observer should check the
   // thread model before handling the real task.
-  virtual bool NotifyVsync(TimeStamp aVsyncTimestamp) = 0;
+  virtual bool NotifyVsync(const VsyncEvent& aVsync) = 0;
 
  protected:
   VsyncObserver() {}
   virtual ~VsyncObserver() {}
 };  // VsyncObserver
 
 // Used to dispatch vsync events in the parent process to compositors.
 //
@@ -44,17 +45,17 @@ class VsyncObserver {
 // compositor thread in the compositor process.
 class CompositorVsyncDispatcher final {
   NS_INLINE_DECL_THREADSAFE_REFCOUNTING(CompositorVsyncDispatcher)
 
  public:
   CompositorVsyncDispatcher();
 
   // Called on the vsync thread when a hardware vsync occurs
-  void NotifyVsync(TimeStamp aVsyncTimestamp);
+  void NotifyVsync(const VsyncEvent& aVsync);
 
   // Compositor vsync observers must be added/removed on the compositor thread
   void SetCompositorVsyncObserver(VsyncObserver* aVsyncObserver);
   void Shutdown();
 
  private:
   virtual ~CompositorVsyncDispatcher();
   void ObserveVsync(bool aEnable);
@@ -67,17 +68,17 @@ class CompositorVsyncDispatcher final {
 // Dispatch vsync event to ipc actor parent and chrome RefreshTimer.
 class RefreshTimerVsyncDispatcher final {
   NS_INLINE_DECL_THREADSAFE_REFCOUNTING(RefreshTimerVsyncDispatcher)
 
  public:
   RefreshTimerVsyncDispatcher();
 
   // Please check CompositorVsyncDispatcher::NotifyVsync().
-  void NotifyVsync(TimeStamp aVsyncTimestamp);
+  void NotifyVsync(const VsyncEvent& aVsync);
 
   // Set chrome process's RefreshTimer to this dispatcher.
   // This function can be called from any thread.
   void SetParentRefreshTimer(VsyncObserver* aVsyncObserver);
 
   // Add or remove the content process' RefreshTimer to this dispatcher. This
   // will be a no-op for AddChildRefreshTimer() if the observer is already
   // registered.