Bug 1510853 - Add CONTENT_FRAME_TIME_REASON. r=jrmuizel, data-review=chutten
☠☠ backed out by 9b7e80071dec ☠ ☠
authorMatt Woodrow <mwoodrow@mozilla.com>
Fri, 07 Dec 2018 17:02:58 +0000
changeset 508835 80baa7b09930c2ea8dc819543d7f500cc9391a84
parent 508834 d1ef6db7fc285d2d914ef98452033fc8517e1914
child 508836 9b7e80071dec2a9f5a06bcafac336c98fdf86951
push id1905
push userffxbld-merge
push dateMon, 21 Jan 2019 12:33:13 +0000
treeherdermozilla-release@c2fca1944d8c [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjrmuizel
bugs1510853
milestone65.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1510853 - Add CONTENT_FRAME_TIME_REASON. r=jrmuizel, data-review=chutten MozReview-Commit-ID: 9RV9ZkHXZTR Differential Revision: https://phabricator.services.mozilla.com/D13351
gfx/layers/ipc/CompositorBridgeParent.cpp
gfx/layers/ipc/CompositorBridgeParent.h
gfx/layers/ipc/CrossProcessCompositorBridgeParent.cpp
gfx/layers/ipc/CrossProcessCompositorBridgeParent.h
gfx/layers/ipc/LayerTransactionParent.cpp
gfx/layers/ipc/LayerTransactionParent.h
gfx/layers/wr/WebRenderBridgeParent.cpp
gfx/layers/wr/WebRenderBridgeParent.h
gfx/webrender_bindings/RenderThread.cpp
gfx/webrender_bindings/RenderThread.h
gfx/webrender_bindings/WebRenderAPI.cpp
toolkit/components/telemetry/Histograms.json
--- 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();
 }
 
@@ -894,17 +895,17 @@ void CompositorBridgeParent::CompositeTo
   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();
   }
@@ -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
@@ -142,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
@@ -309,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;
@@ -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/CrossProcessCompositorBridgeParent.cpp
+++ b/gfx/layers/ipc/CrossProcessCompositorBridgeParent.cpp
@@ -389,27 +389,23 @@ void CrossProcessCompositorBridgeParent:
       static_cast<uint32_t>(
           (endTime - aInfo.transactionStart()).ToMilliseconds()));
 
   aLayerTree->SetPendingTransactionId(
       aInfo.id(), aInfo.vsyncId(), aInfo.refreshStart(),
       aInfo.transactionStart(), aInfo.url(), aInfo.fwdTime());
 }
 
-void CrossProcessCompositorBridgeParent::DidComposite(
-    LayersId aId, TimeStamp& aCompositeStart, TimeStamp& aCompositeEnd) {
-  MonitorAutoLock lock(*sIndirectLayerTreesLock);
-  DidCompositeLocked(aId, aCompositeStart, aCompositeEnd);
-}
-
 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::Success);
+    } 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
@@ -79,17 +79,18 @@ class LayerTransactionParent final : pub
                                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;
--- 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();
@@ -950,17 +952,18 @@ mozilla::ipc::IPCResult WebRenderBridgeP
                            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();
 }
 
@@ -1071,17 +1074,18 @@ mozilla::ipc::IPCResult WebRenderBridgeP
     // 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();
 }
 
@@ -1213,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
@@ -1674,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
@@ -1734,17 +1739,17 @@ 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();
 
@@ -1777,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;
     }
 
@@ -1817,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::Success);
+      } 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
@@ -176,19 +176,19 @@ class WebRenderBridgeParent final : publ
   void HoldPendingTransactionId(
       const wr::Epoch& aWrEpoch, TransactionId aTransactionId,
       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();
@@ -311,17 +311,17 @@ 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, const VsyncId& aVsyncId,
                          const TimeStamp& aRefreshStartTime,
                          const TimeStamp& aTxnStartTime,
                          const nsCString& aTxnURL, const TimeStamp& aFwdTime,
--- 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
@@ -366,18 +366,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/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": ["Success", "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",