Merge mozilla-central to mozilla-inbound. a=merge
authorDaniel Varga <dvarga@mozilla.com>
Sat, 08 Dec 2018 11:50:00 +0200
changeset 449670 4c4645fdcf1cd1a18e03a0689fcc5528e0836a01
parent 449669 a72dafbb2e8089c38cf155f846e2256b90c1aa9e (current diff)
parent 449635 336f58aeb663c01ede2a646d51d5015bf741538d (diff)
child 449671 98f8e4e44c103044d3a6d9a27bd2e8586fe4f05e
push idunknown
push userunknown
push dateunknown
reviewersmerge
milestone65.0a1
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.