Bug 1600793 - Make the scrolling input telemetry work for WebRender r=botond,jrmuizel
☠☠ backed out by 40ca68bfea62 ☠ ☠
authorSean Feng <sefeng@mozilla.com>
Fri, 14 Feb 2020 15:26:35 +0000
changeset 513947 80c254f460f5dcab9b3d111511a52ad97ccb4b32
parent 513946 4a95f898cab860303e46df90d25f48e3f909bf5a
child 513948 59147c470b04a9541dc59ee5df1c3ecf879b2f9d
push id107360
push usersefeng@mozilla.com
push dateFri, 14 Feb 2020 15:37:57 +0000
treeherderautoland@80c254f460f5 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbotond, jrmuizel
bugs1600793
milestone75.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 1600793 - Make the scrolling input telemetry work for WebRender r=botond,jrmuizel Differential Revision: https://phabricator.services.mozilla.com/D60046
gfx/layers/apz/public/APZSampler.h
gfx/layers/apz/src/APZCTreeManager.cpp
gfx/layers/apz/src/APZCTreeManager.h
gfx/layers/apz/src/APZSampler.cpp
gfx/layers/ipc/CompositorBridgeParent.cpp
gfx/layers/wr/WebRenderBridgeParent.cpp
gfx/layers/wr/WebRenderBridgeParent.h
gfx/webrender_bindings/src/bindings.rs
gfx/webrender_bindings/webrender_ffi.h
gfx/wr/webrender/src/lib.rs
gfx/wr/webrender/src/render_backend.rs
gfx/wr/webrender/src/renderer.rs
--- a/gfx/layers/apz/public/APZSampler.h
+++ b/gfx/layers/apz/public/APZSampler.h
@@ -53,23 +53,25 @@ class APZSampler {
 
   /**
    * This function is invoked from rust on the render backend thread when it
    * is created. It effectively tells the APZSampler "the current thread is
    * the sampler thread for this window id" and allows APZSampler to remember
    * which thread it is.
    */
   static void SetSamplerThread(const wr::WrWindowId& aWindowId);
-  static void SampleForWebRender(const wr::WrWindowId& aWindowId,
-                                 wr::Transaction* aTxn,
-                                 const wr::DocumentId& aRenderRootId);
+  static void SampleForWebRender(
+      const wr::WrWindowId& aWindowId, wr::Transaction* aTxn,
+      const wr::DocumentId& aRenderRootId,
+      const wr::WrPipelineIdEpochs& aEpochsBeingRendered);
 
   void SetSampleTime(const TimeStamp& aSampleTime);
   void SampleForWebRender(wr::TransactionWrapper& aTxn,
-                          wr::RenderRoot aRenderRoot);
+                          wr::RenderRoot aRenderRoot,
+                          const wr::WrPipelineIdEpochs& aEpochsBeingRendered);
 
   bool SampleAnimations(const LayerMetricsWrapper& aLayer,
                         const TimeStamp& aSampleTime);
 
   /**
    * Compute the updated shadow transform for a scroll thumb layer that
    * reflects async scrolling of the associated scroll frame.
    *
--- a/gfx/layers/apz/src/APZCTreeManager.cpp
+++ b/gfx/layers/apz/src/APZCTreeManager.cpp
@@ -684,19 +684,20 @@ void APZCTreeManager::UpdateHitTestingTr
     const WebRenderScrollDataWrapper& aScrollWrapper, bool aIsFirstPaint,
     WRRootId aOriginatingWrRootId, uint32_t aPaintSequenceNumber) {
   AssertOnUpdaterThread();
 
   UpdateHitTestingTreeImpl(aScrollWrapper, aIsFirstPaint, aOriginatingWrRootId,
                            aPaintSequenceNumber);
 }
 
-void APZCTreeManager::SampleForWebRender(wr::TransactionWrapper& aTxn,
-                                         const TimeStamp& aSampleTime,
-                                         wr::RenderRoot aRenderRoot) {
+void APZCTreeManager::SampleForWebRender(
+    wr::TransactionWrapper& aTxn, const TimeStamp& aSampleTime,
+    wr::RenderRoot aRenderRoot,
+    const wr::WrPipelineIdEpochs& aEpochsBeingRendered) {
   AssertOnSamplerThread();
   MutexAutoLock lock(mMapLock);
 
   nsTArray<wr::WrTransformProperty> transforms;
 
   // Sample async transforms on scrollable layers.
   for (const auto& mapping : mApzcMap) {
     AsyncPanZoomController* apzc = mapping.second;
@@ -709,16 +710,37 @@ void APZCTreeManager::SampleForWebRender
         apzc->GetZoomAnimationId()
             ? AsyncTransformComponents{AsyncTransformComponent::eLayout}
             : LayoutAndVisual;
     ParentLayerPoint layerTranslation =
         apzc->GetCurrentAsyncTransform(AsyncPanZoomController::eForCompositing,
                                        asyncTransformComponents)
             .mTranslation;
 
+    if (Maybe<CompositionPayload> payload = apzc->NotifyScrollSampling()) {
+      RefPtr<WebRenderBridgeParent> wrBridgeParent;
+      LayersId layersId = apzc->GetGuid().mLayersId;
+      CompositorBridgeParent::CallWithIndirectShadowTree(
+          layersId, [&](LayerTreeState& aState) -> void {
+            wrBridgeParent = aState.mWrBridge;
+          });
+
+      if (wrBridgeParent) {
+        wr::PipelineId pipelineId = wr::AsPipelineId(layersId);
+        for (size_t i = 0; i < aEpochsBeingRendered.length; i++) {
+          if (aEpochsBeingRendered.data[i].pipeline_id == pipelineId) {
+            auto& epoch = aEpochsBeingRendered.data[i].epoch;
+            std::pair<wr::PipelineId, wr::Epoch> key(pipelineId, epoch);
+            wrBridgeParent->AddPendingScrollPayload(*payload, key);
+            break;
+          }
+        }
+      }
+    }
+
     if (Maybe<uint64_t> zoomAnimationId = apzc->GetZoomAnimationId()) {
       // for now we only support zooming on root content APZCs
       MOZ_ASSERT(apzc->IsRootContent());
 
       LayoutDeviceToParentLayerScale zoom =
         apzc->GetCurrentPinchZoomScale(AsyncPanZoomController::eForCompositing);
 
       AsyncTransform asyncVisualTransform = apzc->GetCurrentAsyncTransform(
--- a/gfx/layers/apz/src/APZCTreeManager.h
+++ b/gfx/layers/apz/src/APZCTreeManager.h
@@ -197,17 +197,18 @@ class APZCTreeManager : public IAPZCTree
    * AsyncCompositionManager.
    * In the WebRender world a single "layer tree" might get split into multiple
    * render roots; the aRenderRoot argument indicates which render root we are
    * sampling in this call. The transaction should only be updated with samples
    * from APZC instances in that render root.
    */
   void SampleForWebRender(wr::TransactionWrapper& aTxn,
                           const TimeStamp& aSampleTime,
-                          wr::RenderRoot aRenderRoot);
+                          wr::RenderRoot aRenderRoot,
+                          const wr::WrPipelineIdEpochs& aEpochsBeingRendered);
 
   /**
    * Refer to the documentation of APZInputBridge::ReceiveInputEvent() and
    * APZEventResult.
    */
   APZEventResult ReceiveInputEvent(InputData& aEvent) override;
 
   /**
--- a/gfx/layers/apz/src/APZSampler.cpp
+++ b/gfx/layers/apz/src/APZSampler.cpp
@@ -60,45 +60,48 @@ void APZSampler::SetWebRenderWindowId(co
 void APZSampler::SetSamplerThread(const wr::WrWindowId& aWindowId) {
   if (RefPtr<APZSampler> sampler = GetSampler(aWindowId)) {
     MutexAutoLock lock(sampler->mThreadIdLock);
     sampler->mSamplerThreadId = Some(PlatformThread::CurrentId());
   }
 }
 
 /*static*/
-void APZSampler::SampleForWebRender(const wr::WrWindowId& aWindowId,
-                                    wr::Transaction* aTransaction,
-                                    const wr::DocumentId& aRenderRootId) {
+void APZSampler::SampleForWebRender(
+    const wr::WrWindowId& aWindowId, wr::Transaction* aTransaction,
+    const wr::DocumentId& aRenderRootId,
+    const wr::WrPipelineIdEpochs& aEpochsBeingRendered) {
   if (RefPtr<APZSampler> sampler = GetSampler(aWindowId)) {
     wr::TransactionWrapper txn(aTransaction);
-    sampler->SampleForWebRender(txn, wr::RenderRootFromId(aRenderRootId));
+    sampler->SampleForWebRender(txn, wr::RenderRootFromId(aRenderRootId),
+                                aEpochsBeingRendered);
   }
 }
 
 void APZSampler::SetSampleTime(const TimeStamp& aSampleTime) {
   MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread());
   MutexAutoLock lock(mSampleTimeLock);
   mSampleTime = aSampleTime;
 }
 
-void APZSampler::SampleForWebRender(wr::TransactionWrapper& aTxn,
-                                    wr::RenderRoot aRenderRoot) {
+void APZSampler::SampleForWebRender(
+    wr::TransactionWrapper& aTxn, wr::RenderRoot aRenderRoot,
+    const wr::WrPipelineIdEpochs& aEpochsBeingRendered) {
   AssertOnSamplerThread();
   TimeStamp sampleTime;
   {  // scope lock
     MutexAutoLock lock(mSampleTimeLock);
 
     // If mSampleTime is null we're in a startup phase where the
     // WebRenderBridgeParent hasn't yet provided us with a sample time.
     // If we're that early there probably aren't any APZ animations happening
     // anyway, so using Timestamp::Now() should be fine.
     sampleTime = mSampleTime.IsNull() ? TimeStamp::Now() : mSampleTime;
   }
-  mApz->SampleForWebRender(aTxn, sampleTime, aRenderRoot);
+  mApz->SampleForWebRender(aTxn, sampleTime, aRenderRoot, aEpochsBeingRendered);
 }
 
 bool APZSampler::SampleAnimations(const LayerMetricsWrapper& aLayer,
                                   const TimeStamp& aSampleTime) {
   MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread());
   AssertOnSamplerThread();
 
   // TODO: eventually we can drop the aLayer argument and just walk the APZ
@@ -261,16 +264,18 @@ already_AddRefed<APZSampler> APZSampler:
 
 }  // namespace layers
 }  // namespace mozilla
 
 void apz_register_sampler(mozilla::wr::WrWindowId aWindowId) {
   mozilla::layers::APZSampler::SetSamplerThread(aWindowId);
 }
 
-void apz_sample_transforms(mozilla::wr::WrWindowId aWindowId,
-                           mozilla::wr::Transaction* aTransaction,
-                           mozilla::wr::DocumentId aDocumentId) {
-  mozilla::layers::APZSampler::SampleForWebRender(aWindowId, aTransaction,
-                                                  aDocumentId);
+void apz_sample_transforms(
+    mozilla::wr::WrWindowId aWindowId, mozilla::wr::Transaction* aTransaction,
+    mozilla::wr::DocumentId aDocumentId,
+    mozilla::wr::WrPipelineIdEpochs aEpochsBeingRendered) {
+  mozilla::layers::APZSampler::SampleForWebRender(
+      aWindowId, aTransaction, aDocumentId, aEpochsBeingRendered);
+  wr_pipelineid_and_epoch_delete(aEpochsBeingRendered);
 }
 
 void apz_deregister_sampler(mozilla::wr::WrWindowId aWindowId) {}
--- a/gfx/layers/ipc/CompositorBridgeParent.cpp
+++ b/gfx/layers/ipc/CompositorBridgeParent.cpp
@@ -2326,16 +2326,23 @@ void CompositorBridgeParent::NotifyPipel
     mWrBridge->RemoveEpochDataPriorTo(aEpoch);
 
     if (!mPaused) {
       if (mIsForcedFirstPaint) {
         uiController->NotifyFirstPaint();
         mIsForcedFirstPaint = false;
       }
 
+      std::pair<wr::PipelineId, wr::Epoch> key(aPipelineId, aEpoch);
+      if (nsTArray<CompositionPayload>* payloads =
+              mWrBridge->GetPendingScrollPayload(key)) {
+        RecordCompositionPayloadsPresented(*payloads);
+        mWrBridge->RemovePendingScrollPayload(key);
+      }
+
       TransactionId transactionId = mWrBridge->FlushTransactionIdsForEpoch(
           aEpoch, aCompositeStartId, aCompositeStart, aRenderStart,
           aCompositeEnd, uiController);
       Unused << SendDidComposite(LayersId{0}, transactionId, aCompositeStart,
                                  aCompositeEnd);
 
       nsTArray<ImageCompositeNotificationInfo> notifications;
       mWrBridge->ExtractImageCompositeNotifications(&notifications);
@@ -2346,16 +2353,23 @@ void CompositorBridgeParent::NotifyPipel
     return;
   }
 
   auto wrBridge = mAsyncImageManager->GetWrBridge(aPipelineId);
   if (wrBridge && wrBridge->GetCompositorBridge()) {
     MOZ_ASSERT(!wrBridge->IsRootWebRenderBridgeParent());
     wrBridge->RemoveEpochDataPriorTo(aEpoch);
     if (!mPaused) {
+      std::pair<wr::PipelineId, wr::Epoch> key(aPipelineId, aEpoch);
+      if (nsTArray<CompositionPayload>* payloads =
+              wrBridge->GetPendingScrollPayload(key)) {
+        RecordCompositionPayloadsPresented(*payloads);
+        wrBridge->RemovePendingScrollPayload(key);
+      }
+
       TransactionId transactionId = wrBridge->FlushTransactionIdsForEpoch(
           aEpoch, aCompositeStartId, aCompositeStart, aRenderStart,
           aCompositeEnd, uiController, aStats, &stats);
       Unused << wrBridge->GetCompositorBridge()->SendDidComposite(
           wrBridge->GetLayersId(), transactionId, aCompositeStart,
           aCompositeEnd);
     }
   }
--- a/gfx/layers/wr/WebRenderBridgeParent.cpp
+++ b/gfx/layers/wr/WebRenderBridgeParent.cpp
@@ -1042,16 +1042,34 @@ WebRenderBridgeParent::WriteCollectedFra
   return Api(wr::RenderRoot::Default)->WriteCollectedFrames();
 }
 
 RefPtr<wr::WebRenderAPI::GetCollectedFramesPromise>
 WebRenderBridgeParent::GetCollectedFrames() {
   return Api(wr::RenderRoot::Default)->GetCollectedFrames();
 }
 
+void WebRenderBridgeParent::AddPendingScrollPayload(
+    CompositionPayload& aPayload,
+    const std::pair<wr::PipelineId, wr::Epoch>& aKey) {
+  nsTArray<CompositionPayload>* payloads =
+      mPendingScrollPayloads.LookupOrAdd(aKey);
+  payloads->AppendElement(aPayload);
+}
+
+nsTArray<CompositionPayload>* WebRenderBridgeParent::GetPendingScrollPayload(
+    const std::pair<wr::PipelineId, wr::Epoch>& aKey) const {
+  return mPendingScrollPayloads.Get(aKey);
+}
+
+bool WebRenderBridgeParent::RemovePendingScrollPayload(
+    const std::pair<wr::PipelineId, wr::Epoch>& aKey) {
+  return mPendingScrollPayloads.Remove(aKey);
+}
+
 CompositorBridgeParent* WebRenderBridgeParent::GetRootCompositorBridgeParent()
     const {
   if (!mCompositorBridge) {
     return nullptr;
   }
 
   if (IsRootWebRenderBridgeParent()) {
     // This WebRenderBridgeParent is attached to the root
--- a/gfx/layers/wr/WebRenderBridgeParent.h
+++ b/gfx/layers/wr/WebRenderBridgeParent.h
@@ -47,16 +47,52 @@ namespace layers {
 
 class Compositor;
 class CompositorAnimationStorage;
 class CompositorBridgeParentBase;
 class CompositorVsyncScheduler;
 class AsyncImagePipelineManager;
 class WebRenderImageHost;
 
+class PipelineIdAndEpochHashEntry : public PLDHashEntryHdr {
+ public:
+  typedef const std::pair<wr::PipelineId, wr::Epoch>& KeyType;
+  typedef const std::pair<wr::PipelineId, wr::Epoch>* KeyTypePointer;
+  enum { ALLOW_MEMMOVE = true };
+
+  explicit PipelineIdAndEpochHashEntry(wr::PipelineId aPipelineId,
+                                       wr::Epoch aEpoch)
+      : mValue(aPipelineId, aEpoch) {}
+
+  PipelineIdAndEpochHashEntry(PipelineIdAndEpochHashEntry&& aOther) = default;
+
+  explicit PipelineIdAndEpochHashEntry(KeyTypePointer aKey)
+    : mValue(aKey->first, aKey->second) {}
+
+  ~PipelineIdAndEpochHashEntry() {}
+
+  KeyType GetKey() const { return mValue; }
+
+  bool KeyEquals(KeyTypePointer aKey) const {
+    return mValue.first.mHandle == aKey->first.mHandle &&
+           mValue.first.mNamespace == aKey->first.mNamespace &&
+           mValue.second.mHandle == aKey->second.mHandle;
+  };
+
+  static KeyTypePointer KeyToPointer(KeyType aKey) { return &aKey; }
+
+  static PLDHashNumber HashKey(KeyTypePointer aKey) {
+    return mozilla::HashGeneric(aKey->first.mHandle, aKey->first.mNamespace,
+                                aKey->second.mHandle);
+  }
+
+ private:
+  std::pair<wr::PipelineId, wr::Epoch> mValue;
+};
+
 class WebRenderBridgeParent final
     : public PWebRenderBridgeParent,
       public CompositorVsyncSchedulerOwner,
       public CompositableParentManager,
       public layers::FrameRecorder,
       public SupportsWeakPtr<WebRenderBridgeParent> {
  public:
   MOZ_DECLARE_WEAKREFERENCE_TYPENAME(WebRenderBridgeParent)
@@ -314,16 +350,25 @@ class WebRenderBridgeParent final
    * as data URIs.
    *
    * If there is not currently a recorder, this is a no-op and the promise will
    * be rejected.
    */
   RefPtr<wr::WebRenderAPI::GetCollectedFramesPromise> GetCollectedFrames();
 
   void DisableNativeCompositor();
+  void AddPendingScrollPayload(
+      CompositionPayload& aPayload,
+      const std::pair<wr::PipelineId, wr::Epoch>& aKey);
+
+  nsTArray<CompositionPayload>* GetPendingScrollPayload(
+      const std::pair<wr::PipelineId, wr::Epoch>& aKey) const;
+
+  bool RemovePendingScrollPayload(
+      const std::pair<wr::PipelineId, wr::Epoch>& aKey);
 
  private:
   class ScheduleSharedSurfaceRelease;
 
   explicit WebRenderBridgeParent(const wr::PipelineId& aPipelineId);
   virtual ~WebRenderBridgeParent();
 
   wr::WebRenderAPI* Api(wr::RenderRoot aRenderRoot) {
@@ -552,16 +597,20 @@ class WebRenderBridgeParent final
   // These fields keep track of the latest layer observer epoch values in the
   // child and the parent. mChildLayersObserverEpoch is the latest epoch value
   // received from the child. mParentLayersObserverEpoch is the latest epoch
   // value that we have told BrowserParent about (via ObserveLayerUpdate).
   LayersObserverEpoch mChildLayersObserverEpoch;
   LayersObserverEpoch mParentLayersObserverEpoch;
 
   std::deque<PendingTransactionId> mPendingTransactionIds;
+  // These payloads are being used for SCROLL_PRESENT_LATENCY telemetry
+  nsClassHashtable<PipelineIdAndEpochHashEntry, nsTArray<CompositionPayload>>
+      mPendingScrollPayloads;
+
   std::queue<CompositorAnimationIdsForEpoch> mCompositorAnimationsToDelete;
   wr::Epoch mWrEpoch;
   wr::IdNamespace mIdNamespace;
 
   VsyncId mSkippedCompositeId;
   TimeStamp mMostRecentComposite;
 
   // Kind of clunky, but I can't sort out a more elegant way of getting this to
--- a/gfx/webrender_bindings/src/bindings.rs
+++ b/gfx/webrender_bindings/src/bindings.rs
@@ -24,17 +24,17 @@ use std::os::raw::{c_int};
 use std::time::Duration;
 use gleam::gl;
 
 use webrender::{
     api::*, api::units::*, ApiRecordingReceiver, AsyncPropertySampler, AsyncScreenshotHandle,
     BinaryRecorder, Compositor, DebugFlags, Device,
     NativeSurfaceId, PipelineInfo, ProfilerHooks, RecordedFrameHandle, Renderer, RendererOptions, RendererStats,
     SceneBuilderHooks, ShaderPrecacheFlags, Shaders, ThreadListener, UploadMethod, VertexUsageHint,
-    WrShaders, set_profiler_hooks, CompositorConfig, NativeSurfaceInfo, NativeTileId
+    WrShaders, set_profiler_hooks, CompositorConfig, NativeSurfaceInfo, NativeTileId, FastHashMap
 };
 use thread_profiler::register_thread_with_profiler;
 use moz2d_renderer::Moz2dBlobImageHandler;
 use program_cache::{WrProgramCache, remove_disk_cache};
 use rayon;
 use num_cpus;
 use euclid::SideOffsets2D;
 use nsstring::nsAString;
@@ -808,16 +808,39 @@ impl<'a> From<(&'a(WrPipelineId, WrDocum
             pipeline_id: (tuple.0).0,
             document_id: (tuple.0).1,
             epoch: *tuple.1
         }
     }
 }
 
 #[repr(C)]
+pub struct WrPipelineIdAndEpoch {
+    pipeline_id: WrPipelineId,
+    epoch: WrEpoch
+}
+
+impl<'a> From<(&WrPipelineId, &WrEpoch)> for WrPipelineIdAndEpoch {
+    fn from(tuple: (&WrPipelineId, &WrEpoch)) -> WrPipelineIdAndEpoch {
+        WrPipelineIdAndEpoch {
+            pipeline_id: *tuple.0,
+            epoch: *tuple.1,
+        }
+    }
+}
+
+type WrPipelineIdEpochs = FfiVec<WrPipelineIdAndEpoch>;
+
+#[no_mangle]
+pub unsafe extern "C" fn wr_pipelineid_and_epoch_delete(_result: WrPipelineIdEpochs) {
+    // _result will be dropped here, and the drop impl on FfiVec will free
+    // the underlying vec memory
+}
+
+#[repr(C)]
 pub struct WrRemovedPipeline {
     pipeline_id: WrPipelineId,
     document_id: WrDocumentId,
 }
 
 impl<'a> From<&'a (WrPipelineId, WrDocumentId)> for WrRemovedPipeline {
     fn from(tuple: &(WrPipelineId, WrDocumentId)) -> WrRemovedPipeline {
         WrRemovedPipeline {
@@ -917,17 +940,18 @@ extern "C" {
     fn apz_post_scene_swap(window_id: WrWindowId, pipeline_info: WrPipelineInfo);
     fn apz_run_updater(window_id: WrWindowId);
     fn apz_deregister_updater(window_id: WrWindowId);
 
     // These callbacks are invoked from the render backend thread (aka the APZ
     // sampler thread)
     fn apz_register_sampler(window_id: WrWindowId);
     fn apz_sample_transforms(window_id: WrWindowId, transaction: &mut Transaction,
-                             document_id: WrDocumentId);
+                             document_id: WrDocumentId,
+                             epochs_being_rendered: WrPipelineIdEpochs);
     fn apz_deregister_sampler(window_id: WrWindowId);
 }
 
 struct APZCallbacks {
     window_id: WrWindowId,
 }
 
 impl APZCallbacks {
@@ -999,19 +1023,24 @@ impl SamplerCallback {
     }
 }
 
 impl AsyncPropertySampler for SamplerCallback {
     fn register(&self) {
         unsafe { apz_register_sampler(self.window_id) }
     }
 
-    fn sample(&self, document_id: DocumentId) -> Vec<FrameMsg> {
+    fn sample(&self, document_id: DocumentId,
+              epochs_being_rendered: &FastHashMap<PipelineId, Epoch>) -> Vec<FrameMsg> {
         let mut transaction = Transaction::new();
-        unsafe { apz_sample_transforms(self.window_id, &mut transaction, document_id) };
+        unsafe { apz_sample_transforms(
+	    self.window_id, &mut transaction,
+            document_id, FfiVec::from_vec(epochs_being_rendered.iter()
+					  .map(WrPipelineIdAndEpoch::from).collect())
+	)};
         // TODO: also omta_sample_transforms(...)
         transaction.get_frame_ops()
     }
 
     fn deregister(&self) {
         unsafe { apz_deregister_sampler(self.window_id) }
     }
 }
--- a/gfx/webrender_bindings/webrender_ffi.h
+++ b/gfx/webrender_bindings/webrender_ffi.h
@@ -5,16 +5,22 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #ifndef WR_h
 #define WR_h
 
 #include "mozilla/gfx/Types.h"
 #include "nsTArray.h"
 
+namespace mozilla {
+namespace wr {
+template<typename T>
+struct FfiVec;
+}
+}
 extern "C" {
 
 // ----
 // Functions invoked from Rust code
 // ----
 
 bool is_in_compositor_thread();
 bool is_in_main_thread();
@@ -68,32 +74,36 @@ struct InternerSubReport {
 
 #undef DECLARE_MEMBER
 
 struct Transaction;
 struct WrWindowId;
 struct DocumentId;
 struct WrPipelineInfo;
 
+struct WrPipelineIdAndEpoch;
+using WrPipelineIdEpochs = FfiVec<WrPipelineIdAndEpoch>;
+
 const uint64_t ROOT_CLIP_CHAIN = ~0;
 
 }  // namespace wr
 }  // namespace mozilla
 
 void apz_register_updater(mozilla::wr::WrWindowId aWindowId);
 void apz_pre_scene_swap(mozilla::wr::WrWindowId aWindowId);
 void apz_post_scene_swap(mozilla::wr::WrWindowId aWindowId,
                          mozilla::wr::WrPipelineInfo aInfo);
 void apz_run_updater(mozilla::wr::WrWindowId aWindowId);
 void apz_deregister_updater(mozilla::wr::WrWindowId aWindowId);
 
 void apz_register_sampler(mozilla::wr::WrWindowId aWindowId);
 void apz_sample_transforms(mozilla::wr::WrWindowId aWindowId,
                            mozilla::wr::Transaction* aTransaction,
-                           mozilla::wr::DocumentId aRenderRootId);
+                           mozilla::wr::DocumentId aRenderRootId,
+                           mozilla::wr::WrPipelineIdEpochs aPipelineEpochs);
 void apz_deregister_sampler(mozilla::wr::WrWindowId aWindowId);
 }  // extern "C"
 
 // Work-around wingdi.h define which conflcits with WR color constant
 #pragma push_macro("TRANSPARENT")
 #undef TRANSPARENT
 
 #include "webrender_ffi_generated.h"
--- a/gfx/wr/webrender/src/lib.rs
+++ b/gfx/wr/webrender/src/lib.rs
@@ -212,16 +212,17 @@ pub use crate::frame_builder::ChasePrimi
 pub use crate::prim_store::PrimitiveDebugId;
 pub use crate::profiler::{ProfilerHooks, set_profiler_hooks};
 pub use crate::renderer::{
     AsyncPropertySampler, CpuProfile, DebugFlags, RendererKind, GpuProfile, GraphicsApi,
     GraphicsApiInfo, PipelineInfo, Renderer, RendererError, RendererOptions, RenderResults,
     RendererStats, SceneBuilderHooks, ThreadListener, ShaderPrecacheFlags,
     MAX_VERTEX_TEXTURE_WIDTH,
 };
+pub use crate::internal_types::FastHashMap;
 pub use crate::screen_capture::{AsyncScreenshotHandle, RecordedFrameHandle};
 pub use crate::shade::{Shaders, WrShaders};
 pub use api as webrender_api;
 pub use webrender_build::shader::ProgramSourceDigest;
 pub use crate::picture::{TileDescriptor, TileId, InvalidationReason};
 pub use crate::picture::{PrimitiveCompareResult, PrimitiveCompareResultDetail, CompareHelperResult};
 pub use crate::picture::{TileNode, TileNodeKind, TileSerializer, TileCacheInstanceSerializer, TileOffset, TileCacheLoggerUpdateLists};
 pub use crate::intern::{Update, UpdateKind, ItemUid};
--- a/gfx/wr/webrender/src/render_backend.rs
+++ b/gfx/wr/webrender/src/render_backend.rs
@@ -1480,29 +1480,30 @@ impl RenderBackend {
         mut render_frame: bool,
         invalidate_rendered_frame: bool,
         frame_counter: &mut u32,
         profile_counters: &mut BackendProfileCounters,
         has_built_scene: bool,
     ) {
         let requested_frame = render_frame;
 
+        let requires_frame_build = self.requires_frame_build();
+        let doc = self.documents.get_mut(&document_id).unwrap();
         // If we have a sampler, get more frame ops from it and add them
         // to the transaction. This is a hook to allow the WR user code to
         // fiddle with things after a potentially long scene build, but just
         // before rendering. This is useful for rendering with the latest
         // async transforms.
         if requested_frame || has_built_scene {
             if let Some(ref sampler) = self.sampler {
-                frame_ops.append(&mut sampler.sample(document_id));
+                frame_ops.append(&mut sampler.sample(document_id,
+                                                     &doc.scene.pipeline_epochs));
             }
         }
 
-        let requires_frame_build = self.requires_frame_build();
-        let doc = self.documents.get_mut(&document_id).unwrap();
         doc.has_built_scene |= has_built_scene;
 
         // If there are any additions or removals of clip modes
         // during the scene build, apply them to the data store now.
         if let Some(updates) = interner_updates {
             #[cfg(feature = "capture")]
             {
                 if self.debug_flags.contains(DebugFlags::TILE_CACHE_LOGGING_DBG) {
--- a/gfx/wr/webrender/src/renderer.rs
+++ b/gfx/wr/webrender/src/renderer.rs
@@ -6201,17 +6201,18 @@ pub trait SceneBuilderHooks {
 /// are all called from the render backend thread.
 pub trait AsyncPropertySampler {
     /// This is called exactly once, when the render backend thread is started
     /// and before it processes anything.
     fn register(&self);
     /// This is called for each transaction with the generate_frame flag set
     /// (i.e. that will trigger a render). The list of frame messages returned
     /// are processed as though they were part of the original transaction.
-    fn sample(&self, document_id: DocumentId) -> Vec<FrameMsg>;
+    fn sample(&self, document_id: DocumentId,
+              doc: &FastHashMap<PipelineId, Epoch>) -> Vec<FrameMsg>;
     /// This is called exactly once, when the render backend thread is about to
     /// terminate.
     fn deregister(&self);
 }
 
 bitflags! {
     /// Flags that control how shaders are pre-cached, if at all.
     #[derive(Default)]