Bug 1538710 - Add WR API endpoint for sending multiple transactions r=nical
☠☠ backed out by 2acb3832e7e5 ☠ ☠
authorDoug Thayer <dothayer@mozilla.com>
Mon, 15 Apr 2019 18:56:04 +0000
changeset 469550 ab2083ff97f4
parent 469549 58487e582e9e
child 469551 afa5cc2c6032
push id35874
push userccoroiu@mozilla.com
push dateTue, 16 Apr 2019 04:04:58 +0000
treeherdermozilla-central@be3f40425b52 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersnical
bugs1538710
milestone68.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 1538710 - Add WR API endpoint for sending multiple transactions r=nical We discussed this a bit in Orlando. Essentially, we want to run cleanup operations in texture_cache before all documents' frames, and then be able to ensure that every document generates a frame, because otherwise we will run into problems with evicted cache items used by non-updating- but-still-rendering documents. Accordingly, we need an endpoint to lump all of the transactions that generate frames together. This adds that and builds out all of the plumbing necessary. Differential Revision: https://phabricator.services.mozilla.com/D25132
gfx/layers/apz/public/CompositorController.h
gfx/layers/ipc/CompositorBridgeParent.cpp
gfx/layers/ipc/CompositorBridgeParent.h
gfx/layers/wr/WebRenderBridgeParent.cpp
gfx/layers/wr/WebRenderBridgeParent.h
gfx/webrender_bindings/RenderThread.cpp
gfx/webrender_bindings/WebRenderAPI.cpp
gfx/webrender_bindings/WebRenderAPI.h
gfx/webrender_bindings/src/bindings.rs
gfx/wr/webrender/src/record.rs
gfx/wr/webrender/src/render_backend.rs
gfx/wr/webrender/src/renderer.rs
gfx/wr/webrender/src/scene_builder.rs
gfx/wr/webrender_api/src/api.rs
gfx/wr/wrench/src/binary_frame_reader.rs
gfx/wr/wrench/src/json_frame_writer.rs
gfx/wr/wrench/src/ron_frame_writer.rs
gfx/wr/wrench/src/yaml_frame_writer.rs
--- a/gfx/layers/apz/public/CompositorController.h
+++ b/gfx/layers/apz/public/CompositorController.h
@@ -14,17 +14,18 @@
 namespace mozilla {
 namespace layers {
 
 class CompositorController {
  public:
   NS_INLINE_DECL_PURE_VIRTUAL_REFCOUNTING
 
   virtual void ScheduleRenderOnCompositorThread(
-      const Maybe<wr::RenderRoot>& aRenderRootid = Nothing()) = 0;
+      const nsTArray<wr::RenderRoot>& aRenderRoots =
+          nsTArray<wr::RenderRoot>()) = 0;
   virtual void ScheduleHideAllPluginWindows() = 0;
   virtual void ScheduleShowAllPluginWindows() = 0;
 
  protected:
   virtual ~CompositorController() = default;
 };
 
 }  // namespace layers
--- a/gfx/layers/ipc/CompositorBridgeParent.cpp
+++ b/gfx/layers/ipc/CompositorBridgeParent.cpp
@@ -645,21 +645,21 @@ void CompositorBridgeParent::ActorDestro
   // handling message reception is finished on this thread.
   mSelfRef = this;
   MessageLoop::current()->PostTask(
       NewRunnableMethod("layers::CompositorBridgeParent::DeferredDestroy", this,
                         &CompositorBridgeParent::DeferredDestroy));
 }
 
 void CompositorBridgeParent::ScheduleRenderOnCompositorThread(
-    const Maybe<wr::RenderRoot>& aRenderRoot) {
+    const nsTArray<wr::RenderRoot>& aRenderRoots) {
   MOZ_ASSERT(CompositorLoop());
-  CompositorLoop()->PostTask(NewRunnableMethod<Maybe<wr::RenderRoot>>(
+  CompositorLoop()->PostTask(NewRunnableMethod<nsTArray<wr::RenderRoot>>(
       "layers::CompositorBridgeParent::ScheduleComposition", this,
-      &CompositorBridgeParent::ScheduleComposition, aRenderRoot));
+      &CompositorBridgeParent::ScheduleComposition, aRenderRoots));
 }
 
 void CompositorBridgeParent::InvalidateOnCompositorThread() {
   MOZ_ASSERT(CompositorLoop());
   CompositorLoop()->PostTask(
       NewRunnableMethod("layers::CompositorBridgeParent::Invalidate", this,
                         &CompositorBridgeParent::Invalidate));
 }
@@ -859,28 +859,24 @@ void CompositorBridgeParent::NotifyShado
     mLayerManager->NotifyShadowTreeTransaction();
   }
   if (aScheduleComposite) {
     ScheduleComposition();
   }
 }
 
 void CompositorBridgeParent::ScheduleComposition(
-    const Maybe<wr::RenderRoot>& aRenderRoot) {
+    const nsTArray<wr::RenderRoot>& aRenderRoots) {
   MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread());
   if (mPaused) {
     return;
   }
 
   if (mWrBridge) {
-    if (aRenderRoot.isSome()) {
-      mWrBridge->ScheduleGenerateFrame(aRenderRoot);
-    } else {
-      mWrBridge->ScheduleGenerateFrameAllRenderRoots();
-    }
+    mWrBridge->ScheduleGenerateFrame(aRenderRoots);
   } else {
     mCompositorScheduler->ScheduleComposition();
   }
 }
 
 // Go down the composite layer tree, setting properties to match their
 // content-side counterparts.
 /* static */
@@ -2059,24 +2055,25 @@ void CompositorBridgeParent::DidComposit
     mTxnStartTime = TimeStamp();
     mFwdTime = TimeStamp();
 #endif
     mPendingTransaction = TransactionId{0};
   }
 }
 
 void CompositorBridgeParent::NotifyDidSceneBuild(
-    wr::RenderRoot aRenderRoot, RefPtr<wr::WebRenderPipelineInfo> aInfo) {
+    const nsTArray<wr::RenderRoot>& aRenderRoots,
+    RefPtr<wr::WebRenderPipelineInfo> aInfo) {
   MOZ_ASSERT(CompositorThreadHolder::IsInCompositorThread());
   if (mPaused) {
     return;
   }
 
   if (mWrBridge) {
-    mWrBridge->NotifyDidSceneBuild(aRenderRoot, aInfo);
+    mWrBridge->NotifyDidSceneBuild(aRenderRoots, aInfo);
   } else {
     mCompositorScheduler->ScheduleComposition();
   }
 }
 
 void CompositorBridgeParent::NotifyPipelineRendered(
     const wr::PipelineId& aPipelineId, const wr::Epoch& aEpoch,
     const VsyncId& aCompositeStartId, TimeStamp& aCompositeStart,
--- a/gfx/layers/ipc/CompositorBridgeParent.h
+++ b/gfx/layers/ipc/CompositorBridgeParent.h
@@ -394,17 +394,17 @@ class CompositorBridgeParent final : pub
   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);
-  void NotifyDidSceneBuild(wr::RenderRoot aRenderRoot,
+  void NotifyDidSceneBuild(const nsTArray<wr::RenderRoot>& aRenderRoots,
                            RefPtr<wr::WebRenderPipelineInfo> aInfo);
   RefPtr<AsyncImagePipelineManager> GetAsyncImagePipelineManager() const;
 
   PCompositorWidgetParent* AllocPCompositorWidgetParent(
       const CompositorWidgetInitData& aInitData) override;
   bool DeallocPCompositorWidgetParent(PCompositorWidgetParent* aActor) override;
 
   void ObserveLayersUpdate(LayersId aLayersId, LayersObserverEpoch aEpoch,
@@ -422,28 +422,30 @@ class CompositorBridgeParent final : pub
   static void SetShadowProperties(Layer* aLayer);
 
   void NotifyChildCreated(LayersId aChild);
 
   void AsyncRender();
 
   // Can be called from any thread
   void ScheduleRenderOnCompositorThread(
-      const Maybe<wr::RenderRoot>& aRenderRoot = Nothing()) override;
+      const nsTArray<wr::RenderRoot>& aRenderRoots =
+          nsTArray<wr::RenderRoot>()) override;
   void SchedulePauseOnCompositorThread();
   void InvalidateOnCompositorThread();
   /**
    * Returns true if a surface was obtained and the resume succeeded; false
    * otherwise.
    */
   bool ScheduleResumeOnCompositorThread();
   bool ScheduleResumeOnCompositorThread(int x, int y, int width, int height);
 
-  void ScheduleComposition(
-      const Maybe<wr::RenderRoot>& aRenderRoot = Nothing());
+  void ScheduleComposition(const nsTArray<wr::RenderRoot>& aRenderRoots =
+                               nsTArray<wr::RenderRoot>());
+
   void NotifyShadowTreeTransaction(LayersId aId, bool aIsFirstPaint,
                                    const FocusTarget& aFocusTarget,
                                    bool aScheduleComposite,
                                    uint32_t aPaintSequenceNumber,
                                    bool aIsRepeatTransaction,
                                    bool aHitTestUpdate);
 
   void UpdatePaintTime(LayerTransactionParent* aLayerTree,
--- a/gfx/layers/wr/WebRenderBridgeParent.cpp
+++ b/gfx/layers/wr/WebRenderBridgeParent.cpp
@@ -1997,26 +1997,29 @@ void WebRenderBridgeParent::MaybeGenerat
       Api(wr::RenderRoot::Default)->GetId(), aId, start, framesGenerated);
 
 #if defined(ENABLE_FRAME_LATENCY_LOG)
   auto startTime = TimeStamp::Now();
   Api(wr::RenderRoot::Default)->SetFrameStartTime(startTime);
 #endif
 
   MOZ_ASSERT(framesGenerated > 0);
+  wr::RenderRootArray<wr::TransactionBuilder*> generateFrameTxns;
   for (auto& api : mApis) {
     if (!api) {
       continue;
     }
     auto renderRoot = api->GetRenderRoot();
     if (generateFrame[renderRoot]) {
       fastTxns[renderRoot]->GenerateFrame();
-      api->SendTransaction(*fastTxns[renderRoot]);
+      generateFrameTxns[renderRoot] = fastTxns[renderRoot].ptr();
     }
   }
+  wr::WebRenderAPI::SendTransactions(mApis, generateFrameTxns);
+
   mMostRecentComposite = TimeStamp::Now();
 }
 
 void WebRenderBridgeParent::HoldPendingTransactionId(
     const wr::Epoch& aWrEpoch, TransactionId aTransactionId,
     bool aContainsSVGGroup, const VsyncId& aVsyncId,
     const TimeStamp& aVsyncStartTime, const TimeStamp& aRefreshStartTime,
     const TimeStamp& aTxnStartTime, const nsCString& aTxnURL,
@@ -2054,23 +2057,26 @@ void WebRenderBridgeParent::NotifySceneB
     if (id.mEpoch.mHandle == aEpoch.mHandle) {
       id.mSceneBuiltTime = aEndTime;
       break;
     }
   }
 }
 
 void WebRenderBridgeParent::NotifyDidSceneBuild(
-    wr::RenderRoot aRenderRoot, RefPtr<wr::WebRenderPipelineInfo> aInfo) {
+    const nsTArray<wr::RenderRoot>& aRenderRoots,
+    RefPtr<wr::WebRenderPipelineInfo> aInfo) {
   MOZ_ASSERT(IsRootWebRenderBridgeParent());
   if (!mCompositorScheduler) {
     return;
   }
 
-  mAsyncImageManager->SetWillGenerateFrame(aRenderRoot);
+  for (auto renderRoot : aRenderRoots) {
+    mAsyncImageManager->SetWillGenerateFrame(renderRoot);
+  }
 
   // If the scheduler has a composite more recent than our last composite (which
   // we missed), and we're within the threshold ms of the last vsync, then
   // kick of a late composite.
   TimeStamp lastVsync = mCompositorScheduler->GetLastVsyncTime();
   VsyncId lastVsyncId = mCompositorScheduler->GetLastVsyncId();
   if (lastVsyncId == VsyncId() || !mMostRecentComposite ||
       mMostRecentComposite >= lastVsync ||
@@ -2191,16 +2197,29 @@ void WebRenderBridgeParent::ScheduleGene
   if (mCompositorScheduler) {
     if (aRenderRoot.isSome()) {
       mAsyncImageManager->SetWillGenerateFrame(*aRenderRoot);
     }
     mCompositorScheduler->ScheduleComposition();
   }
 }
 
+void WebRenderBridgeParent::ScheduleGenerateFrame(
+    const nsTArray<wr::RenderRoot>& aRenderRoots) {
+  if (mCompositorScheduler) {
+    if (aRenderRoots.IsEmpty()) {
+      mAsyncImageManager->SetWillGenerateFrameAllRenderRoots();
+    }
+    for (auto renderRoot : aRenderRoots) {
+      mAsyncImageManager->SetWillGenerateFrame(renderRoot);
+    }
+    mCompositorScheduler->ScheduleComposition();
+  }
+}
+
 void WebRenderBridgeParent::FlushRendering(bool aWaitForPresent) {
   if (mDestroyed) {
     return;
   }
 
   // This gets called during e.g. window resizes, so we need to flush the
   // scene (which has the display list at the new window size).
   FlushSceneBuilds();
--- a/gfx/layers/wr/WebRenderBridgeParent.h
+++ b/gfx/layers/wr/WebRenderBridgeParent.h
@@ -226,27 +226,28 @@ class WebRenderBridgeParent final
    * to AsyncImagePipelines. If there is no update, WebRenderBridgeParent skips
    * to generate frame. If we need to generate new frame at next composite
    * timing, call this method.
    *
    * Call CompositorVsyncScheduler::ScheduleComposition() directly, if we just
    * want to trigger AsyncImagePipelines update checks.
    */
   void ScheduleGenerateFrame(const Maybe<wr::RenderRoot>& aRenderRoot);
+  void ScheduleGenerateFrame(const nsTArray<wr::RenderRoot>& aRenderRoots);
   void ScheduleGenerateFrameAllRenderRoots();
 
   /**
    * Schedule forced frame rendering at next composite timing.
    *
    * WebRender could skip frame rendering if there is no update.
    * This function is used to force rendering even when there is not update.
    */
   void ScheduleForcedGenerateFrame();
 
-  void NotifyDidSceneBuild(wr::RenderRoot aRenderRoot,
+  void NotifyDidSceneBuild(const nsTArray<wr::RenderRoot>& aRenderRoots,
                            RefPtr<wr::WebRenderPipelineInfo> aInfo);
 
   wr::Epoch UpdateWebRender(
       CompositorVsyncScheduler* aScheduler,
       nsTArray<RefPtr<wr::WebRenderAPI>>&& aApis,
       AsyncImagePipelineManager* aImageMgr,
       CompositorAnimationStorage* aAnimStorage,
       const TextureFactoryIdentifier& aTextureFactoryIdentifier);
--- a/gfx/webrender_bindings/RenderThread.cpp
+++ b/gfx/webrender_bindings/RenderThread.cpp
@@ -936,36 +936,47 @@ void wr_notifier_external_event(mozilla:
                                 size_t aRawEvent) {
   mozilla::UniquePtr<mozilla::wr::RendererEvent> evt(
       reinterpret_cast<mozilla::wr::RendererEvent*>(aRawEvent));
   mozilla::wr::RenderThread::Get()->RunEvent(mozilla::wr::WindowId(aWindowId),
                                              std::move(evt));
 }
 
 void wr_schedule_render(mozilla::wr::WrWindowId aWindowId,
-                        mozilla::wr::WrDocumentId aDocumentId) {
+                        const mozilla::wr::WrDocumentId* aDocumentIds,
+                        size_t aDocumentIdsCount) {
   RefPtr<mozilla::layers::CompositorBridgeParent> cbp = mozilla::layers::
       CompositorBridgeParent::GetCompositorBridgeParentFromWindowId(aWindowId);
   if (cbp) {
-    cbp->ScheduleRenderOnCompositorThread(
-        Some(wr::RenderRootFromId(aDocumentId)));
+    InfallibleTArray<wr::RenderRoot> renderRoots;
+    renderRoots.SetLength(aDocumentIdsCount);
+    for (size_t i = 0; i < aDocumentIdsCount; ++i) {
+      renderRoots[i] = wr::RenderRootFromId(aDocumentIds[i]);
+    }
+    cbp->ScheduleRenderOnCompositorThread(renderRoots);
   }
 }
 
 static void NotifyDidSceneBuild(RefPtr<layers::CompositorBridgeParent> aBridge,
-                                wr::DocumentId aRenderRootId,
+                                const nsTArray<wr::RenderRoot>& aRenderRoots,
                                 RefPtr<wr::WebRenderPipelineInfo> aInfo) {
-  aBridge->NotifyDidSceneBuild(wr::RenderRootFromId(aRenderRootId), aInfo);
+  aBridge->NotifyDidSceneBuild(aRenderRoots, aInfo);
 }
 
 void wr_finished_scene_build(mozilla::wr::WrWindowId aWindowId,
-                             mozilla::wr::WrDocumentId aDocumentId,
+                             const mozilla::wr::WrDocumentId* aDocumentIds,
+                             size_t aDocumentIdsCount,
                              mozilla::wr::WrPipelineInfo aInfo) {
   RefPtr<mozilla::layers::CompositorBridgeParent> cbp = mozilla::layers::
       CompositorBridgeParent::GetCompositorBridgeParentFromWindowId(aWindowId);
   RefPtr<wr::WebRenderPipelineInfo> info = new wr::WebRenderPipelineInfo(aInfo);
   if (cbp) {
+    InfallibleTArray<wr::RenderRoot> renderRoots;
+    renderRoots.SetLength(aDocumentIdsCount);
+    for (size_t i = 0; i < aDocumentIdsCount; ++i) {
+      renderRoots[i] = wr::RenderRootFromId(aDocumentIds[i]);
+    }
     layers::CompositorThreadHolder::Loop()->PostTask(NewRunnableFunction(
-        "NotifyDidSceneBuild", &NotifyDidSceneBuild, cbp, aDocumentId, info));
+        "NotifyDidSceneBuild", &NotifyDidSceneBuild, cbp, renderRoots, info));
   }
 }
 
 }  // extern C
--- a/gfx/webrender_bindings/WebRenderAPI.cpp
+++ b/gfx/webrender_bindings/WebRenderAPI.cpp
@@ -361,16 +361,49 @@ void WebRenderAPI::UpdateDebugFlags(uint
   }
 }
 
 void WebRenderAPI::SendTransaction(TransactionBuilder& aTxn) {
   UpdateDebugFlags(gfx::gfxVars::WebRenderDebugFlags());
   wr_api_send_transaction(mDocHandle, aTxn.Raw(), aTxn.UseSceneBuilderThread());
 }
 
+/* static */
+void WebRenderAPI::SendTransactions(
+    const RenderRootArray<RefPtr<WebRenderAPI>>& aApis,
+    RenderRootArray<TransactionBuilder*>& aTxns) {
+  if (!aApis[RenderRoot::Default]) {
+    return;
+  }
+
+  aApis[RenderRoot::Default]->UpdateDebugFlags(gfx::gfxVars::WebRenderDebugFlags());
+  AutoTArray<DocumentHandle*, kRenderRootCount> documentHandles;
+  AutoTArray<Transaction*, kRenderRootCount> txns;
+  Maybe<bool> useSceneBuilderThread;
+  for (auto& api : aApis) {
+    if (!api) {
+      continue;
+    }
+    auto& txn = aTxns[api->GetRenderRoot()];
+    if (txn) {
+      documentHandles.AppendElement(api->mDocHandle);
+      txns.AppendElement(txn->Raw());
+      if (useSceneBuilderThread.isSome()) {
+        MOZ_ASSERT(txn->UseSceneBuilderThread() == *useSceneBuilderThread);
+      } else {
+        useSceneBuilderThread.emplace(txn->UseSceneBuilderThread());
+      }
+    }
+  }
+  if (!txns.IsEmpty()) {
+    wr_api_send_transactions(documentHandles.Elements(), txns.Elements(),
+                             txns.Length(), *useSceneBuilderThread);
+  }
+}
+
 bool WebRenderAPI::HitTest(const wr::WorldPoint& aPoint,
                            wr::WrPipelineId& aOutPipelineId,
                            layers::ScrollableLayerGuid::ViewID& aOutScrollId,
                            gfx::CompositorHitTestInfo& aOutHitInfo) {
   static_assert(DoesCompositorHitTestInfoFitIntoBits<16>(),
                 "CompositorHitTestFlags MAX value has to be less than number "
                 "of bits in uint16_t");
 
--- a/gfx/webrender_bindings/WebRenderAPI.h
+++ b/gfx/webrender_bindings/WebRenderAPI.h
@@ -204,16 +204,19 @@ class WebRenderAPI final {
 
  public:
   /// This can be called on the compositor thread only.
   static already_AddRefed<WebRenderAPI> Create(
       layers::CompositorBridgeParent* aBridge,
       RefPtr<widget::CompositorWidget>&& aWidget,
       const wr::WrWindowId& aWindowId, LayoutDeviceIntSize aSize);
 
+  static void SendTransactions(const RenderRootArray<RefPtr<WebRenderAPI>>& aApis,
+                               RenderRootArray<TransactionBuilder*>& aTxns);
+
   already_AddRefed<WebRenderAPI> CreateDocument(LayoutDeviceIntSize aSize,
                                                 int8_t aLayerIndex,
                                                 wr::RenderRoot aRenderRoot);
 
   already_AddRefed<WebRenderAPI> Clone();
 
   wr::WindowId GetId() const { return mId; }
 
--- a/gfx/webrender_bindings/src/bindings.rs
+++ b/gfx/webrender_bindings/src/bindings.rs
@@ -576,18 +576,23 @@ struct CppNotifier {
 unsafe impl Send for CppNotifier {}
 
 extern "C" {
     fn wr_notifier_wake_up(window_id: WrWindowId);
     fn wr_notifier_new_frame_ready(window_id: WrWindowId);
     fn wr_notifier_nop_frame_done(window_id: WrWindowId);
     fn wr_notifier_external_event(window_id: WrWindowId,
                                   raw_event: usize);
-    fn wr_schedule_render(window_id: WrWindowId, document_id: WrDocumentId);
-    fn wr_finished_scene_build(window_id: WrWindowId, document_id: WrDocumentId, pipeline_info: WrPipelineInfo);
+    fn wr_schedule_render(window_id: WrWindowId,
+                          document_id_array: *const WrDocumentId,
+                          document_id_count: usize);
+    fn wr_finished_scene_build(window_id: WrWindowId,
+                               document_id_array: *const WrDocumentId,
+                               document_id_count: usize,
+                               pipeline_info: WrPipelineInfo);
 
     fn wr_transaction_notification_notified(handler: usize, when: Checkpoint);
 }
 
 impl RenderNotifier for CppNotifier {
     fn clone(&self) -> Box<RenderNotifier> {
         Box::new(CppNotifier {
             window_id: self.window_id,
@@ -911,33 +916,33 @@ impl SceneBuilderHooks for APZCallbacks 
 
     fn pre_scene_swap(&self, scenebuild_time: u64) {
         unsafe {
             record_telemetry_time(TelemetryProbe::SceneBuildTime, scenebuild_time);
             apz_pre_scene_swap(self.window_id);
         }
     }
 
-    fn post_scene_swap(&self, document_id: DocumentId, info: PipelineInfo, sceneswap_time: u64) {
+    fn post_scene_swap(&self, document_ids: &Vec<DocumentId>, info: PipelineInfo, sceneswap_time: u64) {
         unsafe {
             let info = WrPipelineInfo::new(&info);
             record_telemetry_time(TelemetryProbe::SceneSwapTime, sceneswap_time);
             apz_post_scene_swap(self.window_id, info);
         }
         let info = WrPipelineInfo::new(&info);
 
         // After a scene swap we should schedule a render for the next vsync,
         // otherwise there's no guarantee that the new scene will get rendered
         // anytime soon
-        unsafe { wr_finished_scene_build(self.window_id, document_id, info) }
+        unsafe { wr_finished_scene_build(self.window_id, document_ids.as_ptr(), document_ids.len(), info) }
         unsafe { gecko_profiler_end_marker(b"SceneBuilding\0".as_ptr() as *const c_char); }
     }
 
-    fn post_resource_update(&self, document_id: DocumentId) {
-        unsafe { wr_schedule_render(self.window_id, document_id) }
+    fn post_resource_update(&self, document_ids: &Vec<DocumentId>) {
+        unsafe { wr_schedule_render(self.window_id, document_ids.as_ptr(), document_ids.len()) }
         unsafe { gecko_profiler_end_marker(b"SceneBuilding\0".as_ptr() as *const c_char); }
     }
 
     fn post_empty_scene_build(&self) {
         unsafe { gecko_profiler_end_marker(b"SceneBuilding\0".as_ptr() as *const c_char); }
     }
 
     fn poke(&self) {
@@ -1716,16 +1721,40 @@ pub extern "C" fn wr_api_send_transactio
         return;
     }
     let new_txn = make_transaction(is_async);
     let txn = mem::replace(transaction, new_txn);
     dh.api.send_transaction(dh.document_id, txn);
 }
 
 #[no_mangle]
+pub unsafe extern "C" fn wr_api_send_transactions(
+    document_handles: *const *const DocumentHandle,
+    transactions: *const *mut Transaction,
+    transaction_count: usize,
+    is_async: bool
+) {
+    if transaction_count == 0 {
+        return;
+    }
+    let mut out_transactions = Vec::with_capacity(transaction_count);
+    let mut out_documents = Vec::with_capacity(transaction_count);
+    for i in 0..transaction_count {
+        let txn = &mut **transactions.offset(i as isize);
+        debug_assert!(!txn.is_empty());
+        let new_txn = make_transaction(is_async);
+        out_transactions.push(mem::replace(txn, new_txn));
+        out_documents.push((**document_handles.offset(i as isize)).document_id);
+    }
+    (**document_handles).api.send_transactions(
+        out_documents,
+        out_transactions);
+}
+
+#[no_mangle]
 pub unsafe extern "C" fn wr_transaction_clear_display_list(
     txn: &mut Transaction,
     epoch: WrEpoch,
     pipeline_id: WrPipelineId,
 ) {
     let preserve_frame_state = true;
     let frame_builder = WebRenderFrameBuilder::new(pipeline_id, LayoutSize::zero());
 
--- a/gfx/wr/webrender/src/record.rs
+++ b/gfx/wr/webrender/src/record.rs
@@ -1,13 +1,13 @@
 /* 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/. */
 
-use api::{ApiMsg, FrameMsg, SceneMsg};
+use api::{ApiMsg, FrameMsg, SceneMsg, TransactionMsg};
 use bincode::serialize;
 use byteorder::{LittleEndian, WriteBytesExt};
 use std::any::TypeId;
 use std::fmt::Debug;
 use std::fs::File;
 use std::io::Write;
 use std::mem;
 use std::path::PathBuf;
@@ -56,39 +56,48 @@ impl ApiRecordingReceiver for BinaryReco
 
     fn write_payload(&mut self, _: u32, data: &[u8]) {
         // signal payload with a 0 length
         self.file.write_u32::<LittleEndian>(0).ok();
         self.write_length_and_data(data);
     }
 }
 
+fn should_record_transaction_msg(msgs: &TransactionMsg) -> bool {
+    if msgs.generate_frame {
+        return true;
+    }
+
+    for msg in &msgs.scene_ops {
+        match *msg {
+            SceneMsg::SetDisplayList { .. } |
+            SceneMsg::SetRootPipeline { .. } => return true,
+            _ => {}
+        }
+    }
+
+    for msg in &msgs.frame_ops {
+        match *msg {
+            FrameMsg::GetScrollNodeState(..) |
+            FrameMsg::HitTest(..) => {}
+            _ => return true,
+        }
+    }
+
+    false
+}
+
 pub fn should_record_msg(msg: &ApiMsg) -> bool {
     match *msg {
         ApiMsg::UpdateResources(..) |
         ApiMsg::AddDocument { .. } |
         ApiMsg::DeleteDocument(..) => true,
-        ApiMsg::UpdateDocument(_, ref msgs) => {
-            if msgs.generate_frame {
-                return true;
-            }
-
-            for msg in &msgs.scene_ops {
-                match *msg {
-                    SceneMsg::SetDisplayList { .. } |
-                    SceneMsg::SetRootPipeline { .. } => return true,
-                    _ => {}
+        ApiMsg::UpdateDocuments(_, ref msgs) => {
+            for msg in msgs {
+                if should_record_transaction_msg(msg) {
+                    return true;
                 }
             }
-
-            for msg in &msgs.frame_ops {
-                match *msg {
-                    FrameMsg::GetScrollNodeState(..) |
-                    FrameMsg::HitTest(..) => {}
-                    _ => return true,
-                }
-            }
-
             false
         }
         _ => false,
     }
 }
--- a/gfx/wr/webrender/src/render_backend.rs
+++ b/gfx/wr/webrender/src/render_backend.rs
@@ -857,67 +857,69 @@ impl RenderBackend {
             sampler.register();
         }
 
         while keep_going {
             profile_scope!("handle_msg");
 
             while let Ok(msg) = self.scene_rx.try_recv() {
                 match msg {
-                    SceneBuilderResult::Transaction(mut txn, result_tx) => {
-                        let has_built_scene = txn.built_scene.is_some();
-                        if let Some(doc) = self.documents.get_mut(&txn.document_id) {
+                    SceneBuilderResult::Transactions(mut txns, result_tx) => {
+                        for mut txn in txns.drain(..) {
+                            let has_built_scene = txn.built_scene.is_some();
+                            if let Some(doc) = self.documents.get_mut(&txn.document_id) {
+
+                                doc.removed_pipelines.append(&mut txn.removed_pipelines);
 
-                            doc.removed_pipelines.append(&mut txn.removed_pipelines);
+                                if let Some(mut built_scene) = txn.built_scene.take() {
+                                    doc.new_async_scene_ready(
+                                        built_scene,
+                                        &mut self.recycler,
+                                    );
+                                }
 
-                            if let Some(mut built_scene) = txn.built_scene.take() {
-                                doc.new_async_scene_ready(
-                                    built_scene,
-                                    &mut self.recycler,
-                                );
+                                if let Some(ref tx) = result_tx {
+                                    let (resume_tx, resume_rx) = channel();
+                                    tx.send(SceneSwapResult::Complete(resume_tx)).unwrap();
+                                    // Block until the post-swap hook has completed on
+                                    // the scene builder thread. We need to do this before
+                                    // we can sample from the sampler hook which might happen
+                                    // in the update_document call below.
+                                    resume_rx.recv().ok();
+                                }
+                            } else {
+                                // The document was removed while we were building it, skip it.
+                                // TODO: we might want to just ensure that removed documents are
+                                // always forwarded to the scene builder thread to avoid this case.
+                                if let Some(ref tx) = result_tx {
+                                    tx.send(SceneSwapResult::Aborted).unwrap();
+                                }
+                                continue;
                             }
 
-                            if let Some(tx) = result_tx {
-                                let (resume_tx, resume_rx) = channel();
-                                tx.send(SceneSwapResult::Complete(resume_tx)).unwrap();
-                                // Block until the post-swap hook has completed on
-                                // the scene builder thread. We need to do this before
-                                // we can sample from the sampler hook which might happen
-                                // in the update_document call below.
-                                resume_rx.recv().ok();
+                            self.resource_cache.add_rasterized_blob_images(
+                                txn.rasterized_blobs.take()
+                            );
+                            if let Some((rasterizer, info)) = txn.blob_rasterizer.take() {
+                                self.resource_cache.set_blob_rasterizer(rasterizer, info);
                             }
-                        } else {
-                            // The document was removed while we were building it, skip it.
-                            // TODO: we might want to just ensure that removed documents are
-                            // always forwarded to the scene builder thread to avoid this case.
-                            if let Some(tx) = result_tx {
-                                tx.send(SceneSwapResult::Aborted).unwrap();
-                            }
-                            continue;
-                        }
 
-                        self.resource_cache.add_rasterized_blob_images(
-                            txn.rasterized_blobs.take()
-                        );
-                        if let Some((rasterizer, info)) = txn.blob_rasterizer.take() {
-                            self.resource_cache.set_blob_rasterizer(rasterizer, info);
+                            self.update_document(
+                                txn.document_id,
+                                txn.resource_updates.take(),
+                                txn.interner_updates.take(),
+                                txn.frame_ops.take(),
+                                txn.notifications.take(),
+                                txn.render_frame,
+                                txn.invalidate_rendered_frame,
+                                &mut frame_counter,
+                                &mut profile_counters,
+                                has_built_scene,
+                            );
                         }
-
-                        self.update_document(
-                            txn.document_id,
-                            txn.resource_updates.take(),
-                            txn.interner_updates.take(),
-                            txn.frame_ops.take(),
-                            txn.notifications.take(),
-                            txn.render_frame,
-                            txn.invalidate_rendered_frame,
-                            &mut frame_counter,
-                            &mut profile_counters,
-                            has_built_scene,
-                        );
                     },
                     SceneBuilderResult::FlushComplete(tx) => {
                         tx.send(()).ok();
                     }
                     SceneBuilderResult::ExternalEvent(evt) => {
                         self.notifier.external_event(evt);
                     }
                     SceneBuilderResult::ClearNamespace(id) => {
@@ -1115,17 +1117,17 @@ impl RenderBackend {
                                     epoch,
                                     pipeline_id,
                                     background: pipeline.background_color,
                                     viewport_size: pipeline.viewport_size,
                                     content_size: pipeline.content_size,
                                     preserve_frame_state: false,
                                 };
                                 let txn = TransactionMsg::scene_message(scene_msg);
-                                r.write_msg(*frame_counter, &ApiMsg::UpdateDocument(*id, txn));
+                                r.write_msg(*frame_counter, &ApiMsg::UpdateDocuments(vec![*id], vec![txn]));
                                 r.write_payload(*frame_counter, &Payload::construct_data(
                                     epoch,
                                     pipeline_id,
                                     pipeline.display_list.data(),
                                 ));
                             }
                         }
 
@@ -1172,122 +1174,135 @@ impl RenderBackend {
                 };
                 self.result_tx.send(msg).unwrap();
                 self.notifier.wake_up();
             }
             ApiMsg::ShutDown => {
                 info!("Recycling stats: {:?}", self.recycler);
                 return false;
             }
-            ApiMsg::UpdateDocument(document_id, transaction_msg) => {
-                self.prepare_transaction(
-                    document_id,
-                    transaction_msg,
+            ApiMsg::UpdateDocuments(document_ids, transaction_msgs) => {
+                self.prepare_transactions(
+                    document_ids,
+                    transaction_msgs,
                     frame_counter,
                     profile_counters,
                 );
             }
         }
 
         true
     }
 
-    fn prepare_transaction(
+    fn prepare_transactions(
         &mut self,
-        document_id: DocumentId,
-        mut transaction_msg: TransactionMsg,
+        document_ids: Vec<DocumentId>,
+        mut transaction_msgs: Vec<TransactionMsg>,
         frame_counter: &mut u32,
         profile_counters: &mut BackendProfileCounters,
     ) {
-        let mut txn = Box::new(Transaction {
-            document_id,
-            display_list_updates: Vec::new(),
-            removed_pipelines: Vec::new(),
-            epoch_updates: Vec::new(),
-            request_scene_build: None,
-            blob_rasterizer: None,
-            blob_requests: Vec::new(),
-            resource_updates: transaction_msg.resource_updates,
-            frame_ops: transaction_msg.frame_ops,
-            rasterized_blobs: Vec::new(),
-            notifications: transaction_msg.notifications,
-            set_root_pipeline: None,
-            render_frame: transaction_msg.generate_frame,
-            invalidate_rendered_frame: transaction_msg.invalidate_rendered_frame,
+        let mut use_scene_builder = transaction_msgs.iter()
+            .any(|transaction_msg| transaction_msg.use_scene_builder_thread);
+        let use_high_priority = transaction_msgs.iter()
+            .any(|transaction_msg| !transaction_msg.low_priority);
+
+        let mut txns : Vec<Box<Transaction>> = document_ids.iter().zip(transaction_msgs.drain(..))
+            .map(|(&document_id, mut transaction_msg)| {
+                let mut txn = Box::new(Transaction {
+                    document_id,
+                    display_list_updates: Vec::new(),
+                    removed_pipelines: Vec::new(),
+                    epoch_updates: Vec::new(),
+                    request_scene_build: None,
+                    blob_rasterizer: None,
+                    blob_requests: Vec::new(),
+                    resource_updates: transaction_msg.resource_updates,
+                    frame_ops: transaction_msg.frame_ops,
+                    rasterized_blobs: Vec::new(),
+                    notifications: transaction_msg.notifications,
+                    set_root_pipeline: None,
+                    render_frame: transaction_msg.generate_frame,
+                    invalidate_rendered_frame: transaction_msg.invalidate_rendered_frame,
+                });
+
+                self.resource_cache.pre_scene_building_update(
+                    &mut txn.resource_updates,
+                    &mut profile_counters.resources,
+                );
+
+                // If we've been above the threshold for reclaiming GPU cache memory for
+                // long enough, drop it and rebuild it. This needs to be done before any
+                // updates for this frame are made.
+                if self.gpu_cache.should_reclaim_memory() {
+                    self.gpu_cache.clear();
+                }
+
+                for scene_msg in transaction_msg.scene_ops.drain(..) {
+                    let _timer = profile_counters.total_time.timer();
+                    self.process_scene_msg(
+                        document_id,
+                        scene_msg,
+                        *frame_counter,
+                        &mut txn,
+                        &mut profile_counters.ipc,
+                    )
+                }
+
+                let blobs_to_rasterize = get_blob_image_updates(&txn.resource_updates);
+                if !blobs_to_rasterize.is_empty() {
+                    let (blob_rasterizer, blob_requests) = self.resource_cache
+                        .create_blob_scene_builder_requests(&blobs_to_rasterize);
+
+                    txn.blob_requests = blob_requests;
+                    txn.blob_rasterizer = blob_rasterizer;
+                }
+                txn
+            }).collect();
+
+        use_scene_builder = use_scene_builder || txns.iter().any(|txn| {
+            !txn.can_skip_scene_builder() || txn.blob_rasterizer.is_some()
         });
 
-        self.resource_cache.pre_scene_building_update(
-            &mut txn.resource_updates,
-            &mut profile_counters.resources,
-        );
-
-        // If we've been above the threshold for reclaiming GPU cache memory for
-        // long enough, drop it and rebuild it. This needs to be done before any
-        // updates for this frame are made.
-        if self.gpu_cache.should_reclaim_memory() {
-            self.gpu_cache.clear();
-        }
-
-        for scene_msg in transaction_msg.scene_ops.drain(..) {
-            let _timer = profile_counters.total_time.timer();
-            self.process_scene_msg(
-                document_id,
-                scene_msg,
-                *frame_counter,
-                &mut txn,
-                &mut profile_counters.ipc,
-            )
-        }
+        if use_scene_builder {
+            for txn in txns.iter_mut() {
+                let doc = self.documents.get_mut(&txn.document_id).unwrap();
 
-        let blobs_to_rasterize = get_blob_image_updates(&txn.resource_updates);
-        if !blobs_to_rasterize.is_empty() {
-            let (blob_rasterizer, blob_requests) = self.resource_cache
-                .create_blob_scene_builder_requests(&blobs_to_rasterize);
-
-            txn.blob_requests = blob_requests;
-            txn.blob_rasterizer = blob_rasterizer;
-        }
-
-        if !transaction_msg.use_scene_builder_thread &&
-            txn.can_skip_scene_builder() &&
-            txn.blob_rasterizer.is_none() {
-
-            self.update_document(
-                txn.document_id,
-                txn.resource_updates.take(),
-                None,
-                txn.frame_ops.take(),
-                txn.notifications.take(),
-                txn.render_frame,
-                txn.invalidate_rendered_frame,
-                frame_counter,
-                profile_counters,
-                false
-            );
-
+                if txn.should_build_scene() {
+                    txn.request_scene_build = Some(SceneRequest {
+                        view: doc.view.clone(),
+                        font_instances: self.resource_cache.get_font_instances(),
+                        output_pipelines: doc.output_pipelines.clone(),
+                    });
+                }
+            }
+        } else {
+            for mut txn in txns {
+                self.update_document(
+                    txn.document_id,
+                    txn.resource_updates.take(),
+                    None,
+                    txn.frame_ops.take(),
+                    txn.notifications.take(),
+                    txn.render_frame,
+                    txn.invalidate_rendered_frame,
+                    frame_counter,
+                    profile_counters,
+                    false
+                );                
+            }
             return;
         }
 
-        let doc = self.documents.get_mut(&document_id).unwrap();
-
-        if txn.should_build_scene() {
-            txn.request_scene_build = Some(SceneRequest {
-                view: doc.view.clone(),
-                font_instances: self.resource_cache.get_font_instances(),
-                output_pipelines: doc.output_pipelines.clone(),
-            });
-        }
-
-        let tx = if transaction_msg.low_priority {
+        let tx = if use_high_priority {
+            &self.scene_tx
+        } else {
             &self.low_priority_scene_tx
-        } else {
-            &self.scene_tx
         };
 
-        tx.send(SceneBuilderRequest::Transaction(txn)).unwrap();
+        tx.send(SceneBuilderRequest::Transactions(txns)).unwrap();
     }
 
     fn update_document(
         &mut self,
         document_id: DocumentId,
         resource_updates: Vec<ResourceUpdate>,
         interner_updates: Option<InternerUpdates>,
         mut frame_ops: Vec<FrameMsg>,
--- a/gfx/wr/webrender/src/renderer.rs
+++ b/gfx/wr/webrender/src/renderer.rs
@@ -5491,21 +5491,21 @@ pub trait SceneBuilderHooks {
     fn register(&self);
     /// This is called before each scene build starts.
     fn pre_scene_build(&self);
     /// This is called before each scene swap occurs.
     fn pre_scene_swap(&self, scenebuild_time: u64);
     /// This is called after each scene swap occurs. The PipelineInfo contains
     /// the updated epochs and pipelines removed in the new scene compared to
     /// the old scene.
-    fn post_scene_swap(&self, document_id: DocumentId, info: PipelineInfo, sceneswap_time: u64);
+    fn post_scene_swap(&self, document_id: &Vec<DocumentId>, info: PipelineInfo, sceneswap_time: u64);
     /// This is called after a resource update operation on the scene builder
     /// thread, in the case where resource updates were applied without a scene
     /// build.
-    fn post_resource_update(&self, document_id: DocumentId);
+    fn post_resource_update(&self, document_ids: &Vec<DocumentId>);
     /// This is called after a scene build completes without any changes being
     /// made. We guarantee that each pre_scene_build call will be matched with
     /// exactly one of post_scene_swap, post_resource_update or
     /// post_empty_scene_build.
     fn post_empty_scene_build(&self);
     /// This is a generic callback which provides an opportunity to run code
     /// on the scene builder thread. This is called as part of the main message
     /// loop of the scene builder thread, but outside of any specific message
--- a/gfx/wr/webrender/src/scene_builder.rs
+++ b/gfx/wr/webrender/src/scene_builder.rs
@@ -23,16 +23,17 @@ use prim_store::gradient::{LinearGradien
 use prim_store::image::{Image, YuvImage};
 use prim_store::line_dec::LineDecoration;
 use prim_store::picture::Picture;
 use prim_store::text_run::TextRun;
 use resource_cache::{AsyncBlobImageInfo, FontInstanceMap};
 use render_backend::DocumentView;
 use renderer::{PipelineInfo, SceneBuilderHooks};
 use scene::Scene;
+use std::iter;
 use std::sync::mpsc::{channel, Receiver, Sender};
 use std::mem::replace;
 use time::precise_time_ns;
 use util::drain_filter;
 use std::thread;
 use std::time::Duration;
 
 
@@ -131,17 +132,17 @@ pub struct LoadScene {
 pub struct BuiltScene {
     pub scene: Scene,
     pub frame_builder: FrameBuilder,
     pub clip_scroll_tree: ClipScrollTree,
 }
 
 // Message from render backend to scene builder.
 pub enum SceneBuilderRequest {
-    Transaction(Box<Transaction>),
+    Transactions(Vec<Box<Transaction>>),
     ExternalEvent(ExternalEvent),
     DeleteDocument(DocumentId),
     WakeUp,
     Flush(MsgSender<()>),
     ClearNamespace(IdNamespace),
     SetFrameBuilderConfig(FrameBuilderConfig),
     SimulateLongSceneBuild(u32),
     SimulateLongLowPrioritySceneBuild(u32),
@@ -150,17 +151,17 @@ pub enum SceneBuilderRequest {
     #[cfg(feature = "capture")]
     SaveScene(CaptureConfig),
     #[cfg(feature = "replay")]
     LoadScenes(Vec<LoadScene>),
 }
 
 // Message from scene builder to render backend.
 pub enum SceneBuilderResult {
-    Transaction(Box<BuiltTransaction>, Option<Sender<SceneSwapResult>>),
+    Transactions(Vec<Box<BuiltTransaction>>, Option<Sender<SceneSwapResult>>),
     ExternalEvent(ExternalEvent),
     FlushComplete(MsgSender<()>),
     ClearNamespace(IdNamespace),
     Stopped,
 }
 
 // Message from render backend to scene builder to indicate the
 // scene swap was completed. We need a separate channel for this
@@ -316,19 +317,21 @@ impl SceneBuilder {
         }
 
         loop {
             match self.rx.recv() {
                 Ok(SceneBuilderRequest::WakeUp) => {}
                 Ok(SceneBuilderRequest::Flush(tx)) => {
                     self.send(SceneBuilderResult::FlushComplete(tx));
                 }
-                Ok(SceneBuilderRequest::Transaction(mut txn)) => {
-                    let built_txn = self.process_transaction(&mut txn);
-                    self.forward_built_transaction(built_txn);
+                Ok(SceneBuilderRequest::Transactions(mut txns)) => {
+                    let built_txns : Vec<Box<BuiltTransaction>> = txns.iter_mut()
+                        .map(|txn| self.process_transaction(txn))
+                        .collect();
+                    self.forward_built_transactions(built_txns);
                 }
                 Ok(SceneBuilderRequest::DeleteDocument(document_id)) => {
                     self.documents.remove(&document_id);
                 }
                 Ok(SceneBuilderRequest::SetFrameBuilderConfig(cfg)) => {
                     self.config = cfg;
                 }
                 Ok(SceneBuilderRequest::ClearNamespace(id)) => {
@@ -424,33 +427,33 @@ impl SceneBuilder {
                 item.document_id,
                 Document {
                     scene: item.scene,
                     interners: item.interners,
                     doc_stats: DocumentStats::empty(),
                 },
             );
 
-            let txn = Box::new(BuiltTransaction {
+            let txns = vec![Box::new(BuiltTransaction {
                 document_id: item.document_id,
                 render_frame: item.build_frame,
                 invalidate_rendered_frame: false,
                 built_scene,
                 resource_updates: Vec::new(),
                 rasterized_blobs: Vec::new(),
                 blob_rasterizer: None,
                 frame_ops: Vec::new(),
                 removed_pipelines: Vec::new(),
                 notifications: Vec::new(),
                 scene_build_start_time,
                 scene_build_end_time: precise_time_ns(),
                 interner_updates,
-            });
+            })];
 
-            self.forward_built_transaction(txn);
+            self.forward_built_transactions(txns);
         }
     }
 
     /// Do the bulk of the work of the scene builder thread.
     fn process_transaction(&mut self, txn: &mut Transaction) -> Box<BuiltTransaction> {
         if let &Some(ref hooks) = &self.hooks {
             hooks.pre_scene_build();
         }
@@ -545,65 +548,78 @@ impl SceneBuilder {
             removed_pipelines: replace(&mut txn.removed_pipelines, Vec::new()),
             notifications: replace(&mut txn.notifications, Vec::new()),
             interner_updates,
             scene_build_start_time,
             scene_build_end_time: precise_time_ns(),
         })
     }
 
-    /// Send the result of process_transaction back to the render backend.
-    fn forward_built_transaction(&mut self, txn: Box<BuiltTransaction>) {
-        // We only need the pipeline info and the result channel if we
-        // have a hook callback *and* if this transaction actually built
-        // a new scene that is going to get swapped in. In other cases
-        // pipeline_info can be None and we can avoid some overhead from
-        // invoking the hooks and blocking on the channel.
-        let (pipeline_info, result_tx, result_rx) = match (&self.hooks, &txn.built_scene) {
-            (&Some(ref hooks), &Some(ref built)) => {
-                let info = PipelineInfo {
-                    epochs: built.scene.pipeline_epochs.iter()
-                        .map(|(&pipeline_id, &epoch)| ((pipeline_id, txn.document_id), epoch))
-                        .collect(),
-                    removed_pipelines: txn.removed_pipelines.clone(),
-                };
-                let (tx, rx) = channel();
+    /// Send the results of process_transaction back to the render backend.
+    fn forward_built_transactions(&mut self, txns: Vec<Box<BuiltTransaction>>) {
+        let (pipeline_info, result_tx, result_rx) = match &self.hooks {
+            &Some(ref hooks) => {
+                if txns.iter().any(|txn| txn.built_scene.is_some()) {
+                    let info = PipelineInfo {
+                        epochs: txns.iter()
+                            .filter(|txn| txn.built_scene.is_some())
+                            .map(|txn| {
+                                txn.built_scene.as_ref().unwrap()
+                                    .scene.pipeline_epochs.iter()
+                                    .zip(iter::repeat(txn.document_id))
+                                    .map(|((&pipeline_id, &epoch), document_id)| ((pipeline_id, document_id), epoch))
+                            }).flatten().collect(),
+                        removed_pipelines: txns.iter()
+                            .map(|txn| txn.removed_pipelines.clone())
+                            .flatten().collect(),
+                    };
 
-                hooks.pre_scene_swap(txn.scene_build_end_time - txn.scene_build_start_time);
+                    let (tx, rx) = channel();
+                    let txn = txns.iter().find(|txn| txn.built_scene.is_some()).unwrap();
+                    hooks.pre_scene_swap(txn.scene_build_end_time - txn.scene_build_start_time);
 
-                (Some(info), Some(tx), Some(rx))
+                    (Some(info), Some(tx), Some(rx))
+                } else {
+                    (None, None, None)
+                }
             }
-            _ => (None, None, None),
+            _ => (None, None, None)
         };
 
-        let document_id = txn.document_id;
         let scene_swap_start_time = precise_time_ns();
-        let has_resources_updates = !txn.resource_updates.is_empty();
-        let invalidate_rendered_frame = txn.invalidate_rendered_frame;
+        let document_ids = txns.iter().map(|txn| txn.document_id).collect();
+        let have_resources_updates : Vec<DocumentId> = if pipeline_info.is_none() {
+            txns.iter()
+                .filter(|txn| !txn.resource_updates.is_empty() || txn.invalidate_rendered_frame)
+                .map(|txn| txn.document_id.clone())
+                .collect()
+        } else {
+            Vec::new()
+        };
 
-        self.tx.send(SceneBuilderResult::Transaction(txn, result_tx)).unwrap();
+        self.tx.send(SceneBuilderResult::Transactions(txns, result_tx)).unwrap();
 
         let _ = self.api_tx.send(ApiMsg::WakeUp);
 
         if let Some(pipeline_info) = pipeline_info {
             // Block until the swap is done, then invoke the hook.
             let swap_result = result_rx.unwrap().recv();
             let scene_swap_time = precise_time_ns() - scene_swap_start_time;
-            self.hooks.as_ref().unwrap().post_scene_swap(document_id,
+            self.hooks.as_ref().unwrap().post_scene_swap(&document_ids,
                                                          pipeline_info, scene_swap_time);
             // Once the hook is done, allow the RB thread to resume
             match swap_result {
                 Ok(SceneSwapResult::Complete(resume_tx)) => {
                     resume_tx.send(()).ok();
                 },
                 _ => (),
             };
-        } else if has_resources_updates || invalidate_rendered_frame {
+        } else if !have_resources_updates.is_empty() {
             if let &Some(ref hooks) = &self.hooks {
-                hooks.post_resource_update(document_id);
+                hooks.post_resource_update(&have_resources_updates);
             }
         } else {
             if let &Some(ref hooks) = &self.hooks {
                 hooks.post_empty_scene_build();
             }
         }
     }
 
@@ -629,19 +645,21 @@ pub struct LowPrioritySceneBuilder {
     pub tx: Sender<SceneBuilderRequest>,
     pub simulate_slow_ms: u32,
 }
 
 impl LowPrioritySceneBuilder {
     pub fn run(&mut self) {
         loop {
             match self.rx.recv() {
-                Ok(SceneBuilderRequest::Transaction(txn)) => {
-                    let txn = self.process_transaction(txn);
-                    self.tx.send(SceneBuilderRequest::Transaction(txn)).unwrap();
+                Ok(SceneBuilderRequest::Transactions(mut txns)) => {
+                    let txns : Vec<Box<Transaction>> = txns.drain(..)
+                        .map(|txn| self.process_transaction(txn))
+                        .collect();
+                    self.tx.send(SceneBuilderRequest::Transactions(txns)).unwrap();
                 }
                 Ok(SceneBuilderRequest::DeleteDocument(document_id)) => {
                     self.tx.send(SceneBuilderRequest::DeleteDocument(document_id)).unwrap();
                 }
                 Ok(SceneBuilderRequest::Stop) => {
                     self.tx.send(SceneBuilderRequest::Stop).unwrap();
                     break;
                 }
--- a/gfx/wr/webrender_api/src/api.rs
+++ b/gfx/wr/webrender_api/src/api.rs
@@ -434,16 +434,21 @@ impl Transaction {
         self.resource_updates.append(&mut other);
     }
 
     pub fn clear(&mut self) {
         self.resource_updates.clear()
     }
 }
 
+pub struct DocumentTransaction {
+    pub document_id: DocumentId,
+    pub transaction: Transaction,
+}
+
 /// Represents a transaction in the format sent through the channel.
 #[derive(Clone, Deserialize, Serialize)]
 pub struct TransactionMsg {
     pub scene_ops: Vec<SceneMsg>,
     pub frame_ops: Vec<FrameMsg>,
     pub resource_updates: Vec<ResourceUpdate>,
     pub generate_frame: bool,
     pub invalidate_rendered_frame: bool,
@@ -721,17 +726,17 @@ pub enum ApiMsg {
     GetGlyphIndices(font::FontKey, String, MsgSender<Vec<Option<u32>>>),
     /// Adds a new document namespace.
     CloneApi(MsgSender<IdNamespace>),
     /// Adds a new document namespace.
     CloneApiByClient(IdNamespace),
     /// Adds a new document with given initial size.
     AddDocument(DocumentId, FramebufferIntSize, DocumentLayer),
     /// A message targeted at a particular document.
-    UpdateDocument(DocumentId, TransactionMsg),
+    UpdateDocuments(Vec<DocumentId>, Vec<TransactionMsg>),
     /// Deletes an existing document.
     DeleteDocument(DocumentId),
     /// An opaque handle that must be passed to the render notifier. It is used by Gecko
     /// to forward gecko-specific messages to the render thread preserving the ordering
     /// within the other messages.
     ExternalEvent(ExternalEvent),
     /// Removes all resources associated with a namespace.
     ClearNamespace(IdNamespace),
@@ -753,17 +758,17 @@ impl fmt::Debug for ApiMsg {
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
         f.write_str(match *self {
             ApiMsg::UpdateResources(..) => "ApiMsg::UpdateResources",
             ApiMsg::GetGlyphDimensions(..) => "ApiMsg::GetGlyphDimensions",
             ApiMsg::GetGlyphIndices(..) => "ApiMsg::GetGlyphIndices",
             ApiMsg::CloneApi(..) => "ApiMsg::CloneApi",
             ApiMsg::CloneApiByClient(..) => "ApiMsg::CloneApiByClient",
             ApiMsg::AddDocument(..) => "ApiMsg::AddDocument",
-            ApiMsg::UpdateDocument(..) => "ApiMsg::UpdateDocument",
+            ApiMsg::UpdateDocuments(..) => "ApiMsg::UpdateDocuments",
             ApiMsg::DeleteDocument(..) => "ApiMsg::DeleteDocument",
             ApiMsg::ExternalEvent(..) => "ApiMsg::ExternalEvent",
             ApiMsg::ClearNamespace(..) => "ApiMsg::ClearNamespace",
             ApiMsg::MemoryPressure => "ApiMsg::MemoryPressure",
             ApiMsg::ReportMemory(..) => "ApiMsg::ReportMemory",
             ApiMsg::DebugCommand(..) => "ApiMsg::DebugCommand",
             ApiMsg::ShutDown => "ApiMsg::ShutDown",
             ApiMsg::WakeUp => "ApiMsg::WakeUp",
@@ -1211,36 +1216,53 @@ impl RenderApi {
     }
 
     /// A helper method to send document messages.
     fn send_scene_msg(&self, document_id: DocumentId, msg: SceneMsg) {
         // This assertion fails on Servo use-cases, because it creates different
         // `RenderApi` instances for layout and compositor.
         //assert_eq!(document_id.0, self.namespace_id);
         self.api_sender
-            .send(ApiMsg::UpdateDocument(document_id, TransactionMsg::scene_message(msg)))
+            .send(ApiMsg::UpdateDocuments(vec![document_id], vec![TransactionMsg::scene_message(msg)]))
             .unwrap()
     }
 
     /// A helper method to send document messages.
     fn send_frame_msg(&self, document_id: DocumentId, msg: FrameMsg) {
         // This assertion fails on Servo use-cases, because it creates different
         // `RenderApi` instances for layout and compositor.
         //assert_eq!(document_id.0, self.namespace_id);
         self.api_sender
-            .send(ApiMsg::UpdateDocument(document_id, TransactionMsg::frame_message(msg)))
+            .send(ApiMsg::UpdateDocuments(vec![document_id], vec![TransactionMsg::frame_message(msg)]))
             .unwrap()
     }
 
     pub fn send_transaction(&self, document_id: DocumentId, transaction: Transaction) {
         let (msg, payloads) = transaction.finalize();
         for payload in payloads {
             self.payload_sender.send_payload(payload).unwrap();
         }
-        self.api_sender.send(ApiMsg::UpdateDocument(document_id, msg)).unwrap();
+        self.api_sender.send(ApiMsg::UpdateDocuments(vec![document_id], vec![msg])).unwrap();
+    }
+
+    pub fn send_transactions(&self, document_ids: Vec<DocumentId>, mut transactions: Vec<Transaction>) {
+        debug_assert!(document_ids.len() == transactions.len());
+        let length = document_ids.len();
+        let (msgs, mut document_payloads) = transactions.drain(..)
+            .fold((Vec::with_capacity(length), Vec::with_capacity(length)),
+                  |(mut msgs, mut document_payloads), transaction| {
+                    let (msg, payloads) = transaction.finalize();
+                    msgs.push(msg);
+                    document_payloads.push(payloads);
+                    (msgs, document_payloads)
+                  });
+        for payload in document_payloads.drain(..).flatten() {
+            self.payload_sender.send_payload(payload).unwrap();
+        }
+        self.api_sender.send(ApiMsg::UpdateDocuments(document_ids.clone(), msgs)).unwrap();
     }
 
     /// Does a hit test on display items in the specified document, at the given
     /// point. If a pipeline_id is specified, it is used to further restrict the
     /// hit results so that only items inside that pipeline are matched. If the
     /// HitTestFlags argument contains the FIND_ALL flag, then the vector of hit
     /// results will contain all display items that match, ordered from front
     /// to back.
--- a/gfx/wr/wrench/src/binary_frame_reader.rs
+++ b/gfx/wr/wrench/src/binary_frame_reader.rs
@@ -135,31 +135,34 @@ impl WrenchThing for BinaryFrameReader {
                     let msg = deserialize(&buffer).unwrap();
                     let mut store_message = true;
                     // In order to detect the first valid frame, we
                     // need to find:
                     // (a) SetRootPipeline
                     // (b) SetDisplayList
                     // (c) GenerateFrame that occurs *after* (a) and (b)
                     match msg {
-                        ApiMsg::UpdateDocument(_, ref txn) => {
-                            if txn.generate_frame {
-                                found_frame_marker = true;
-                            }
-                            for doc_msg in &txn.scene_ops {
-                                match *doc_msg {
-                                    SceneMsg::SetDisplayList { .. } => {
-                                        found_frame_marker = false;
-                                        found_display_list = true;
+                        ApiMsg::UpdateDocuments(_, ref txns) => {
+                            for txn in txns {
+                                if txn.generate_frame {
+                                    // TODO: is this appropriate, or do we need a ternary value / something else?
+                                    found_frame_marker = true;
+                                }
+                                for doc_msg in &txn.scene_ops {
+                                    match *doc_msg {
+                                        SceneMsg::SetDisplayList { .. } => {
+                                            found_frame_marker = false;
+                                            found_display_list = true;
+                                        }
+                                        SceneMsg::SetRootPipeline(..) => {
+                                            found_frame_marker = false;
+                                            found_pipeline = true;
+                                        }
+                                        _ => {}
                                     }
-                                    SceneMsg::SetRootPipeline(..) => {
-                                        found_frame_marker = false;
-                                        found_pipeline = true;
-                                    }
-                                    _ => {}
                                 }
                             }
                         }
                         // Wrench depends on the document always existing
                         ApiMsg::DeleteDocument(_) => {
                             store_message = false;
                         }
                         _ => {}
--- a/gfx/wr/wrench/src/json_frame_writer.rs
+++ b/gfx/wr/wrench/src/json_frame_writer.rs
@@ -266,51 +266,56 @@ impl JsonFrameWriter {
             return None;
         }
 
         data.path = Some(path.clone());
         // put it back
         self.images.insert(key, data);
         Some(path)
     }
+
+    fn update_document(&mut self, txn: &TransactionMsg) {
+        self.update_resources(&txn.resource_updates);
+        for doc_msg in &txn.scene_ops {
+            match *doc_msg {
+                SceneMsg::SetDisplayList {
+                    ref epoch,
+                    ref pipeline_id,
+                    ref background,
+                    ref viewport_size,
+                    ref list_descriptor,
+                    ..
+                } => {
+                    self.begin_write_display_list(
+                        epoch,
+                        pipeline_id,
+                        background,
+                        viewport_size,
+                        list_descriptor,
+                    );
+                }
+                _ => {}
+            }
+        }
+    }
 }
 
 impl fmt::Debug for JsonFrameWriter {
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
         write!(f, "JsonFrameWriter")
     }
 }
 
 impl webrender::ApiRecordingReceiver for JsonFrameWriter {
     fn write_msg(&mut self, _: u32, msg: &ApiMsg) {
         match *msg {
             ApiMsg::UpdateResources(ref updates) => self.update_resources(updates),
-
-            ApiMsg::UpdateDocument(_, ref txn) => {
-                self.update_resources(&txn.resource_updates);
-                for doc_msg in &txn.scene_ops {
-                    match *doc_msg {
-                        SceneMsg::SetDisplayList {
-                            ref epoch,
-                            ref pipeline_id,
-                            ref background,
-                            ref viewport_size,
-                            ref list_descriptor,
-                            ..
-                        } => {
-                            self.begin_write_display_list(
-                                epoch,
-                                pipeline_id,
-                                background,
-                                viewport_size,
-                                list_descriptor,
-                            );
-                        }
-                        _ => {}
-                    }
+            ApiMsg::UpdateDocuments(_, ref txns) => {
+                for txn in txns {
+                    self.update_document(txn)
                 }
             }
             ApiMsg::CloneApi(..) => {}
             _ => {}
         }
     }
 
     fn write_payload(&mut self, frame: u32, data: &[u8]) {
--- a/gfx/wr/wrench/src/ron_frame_writer.rs
+++ b/gfx/wr/wrench/src/ron_frame_writer.rs
@@ -144,50 +144,56 @@ impl RonFrameWriter {
                 },
                 ResourceUpdate::DeleteFont(_) => {}
                 ResourceUpdate::AddFontInstance(_) => {}
                 ResourceUpdate::DeleteFontInstance(_) => {}
                 ResourceUpdate::SetBlobImageVisibleArea(..) => {}
             }
         }
     }
+
+    fn update_document(&mut self, txn: &TransactionMsg) {
+        self.update_resources(&txn.resource_updates);
+        for doc_msg in &txn.scene_ops {
+            match *doc_msg {
+                SceneMsg::SetDisplayList {
+                    ref epoch,
+                    ref pipeline_id,
+                    ref background,
+                    ref viewport_size,
+                    ref list_descriptor,
+                    ..
+                } => {
+                    self.begin_write_display_list(
+                        epoch,
+                        pipeline_id,
+                        background,
+                        viewport_size,
+                        list_descriptor,
+                    );
+                }
+                _ => {}
+            }
+        }
+    }
 }
 
 impl fmt::Debug for RonFrameWriter {
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
         write!(f, "RonFrameWriter")
     }
 }
 
 impl webrender::ApiRecordingReceiver for RonFrameWriter {
     fn write_msg(&mut self, _: u32, msg: &ApiMsg) {
         match *msg {
             ApiMsg::UpdateResources(ref updates) => self.update_resources(updates),
-            ApiMsg::UpdateDocument(_, ref txn) => {
-                self.update_resources(&txn.resource_updates);
-                for doc_msg in &txn.scene_ops {
-                    match *doc_msg {
-                        SceneMsg::SetDisplayList {
-                            ref epoch,
-                            ref pipeline_id,
-                            ref background,
-                            ref viewport_size,
-                            ref list_descriptor,
-                            ..
-                        } => {
-                            self.begin_write_display_list(
-                                epoch,
-                                pipeline_id,
-                                background,
-                                viewport_size,
-                                list_descriptor,
-                            );
-                        }
-                        _ => {}
-                    }
+            ApiMsg::UpdateDocuments(_, ref txns) => {
+                for txn in txns {
+                    self.update_document(txn)
                 }
             }
             ApiMsg::CloneApi(..) => {}
             _ => {}
         }
     }
 
     fn write_payload(&mut self, frame: u32, data: &[u8]) {
--- a/gfx/wr/wrench/src/yaml_frame_writer.rs
+++ b/gfx/wr/wrench/src/yaml_frame_writer.rs
@@ -438,16 +438,56 @@ pub struct YamlFrameWriterReceiver {
 
 impl YamlFrameWriterReceiver {
     pub fn new(path: &Path) -> YamlFrameWriterReceiver {
         YamlFrameWriterReceiver {
             frame_writer: YamlFrameWriter::new(path),
             scene: Scene::new(),
         }
     }
+
+    fn update_document(&mut self, txn: &TransactionMsg) {
+        self.frame_writer.update_resources(&txn.resource_updates);
+        for doc_msg in &txn.scene_ops {
+            match *doc_msg {
+                SceneMsg::SetDisplayList {
+                    ref epoch,
+                    ref pipeline_id,
+                    ref background,
+                    ref viewport_size,
+                    ref list_descriptor,
+                    ..
+                } => {
+                    self.frame_writer.begin_write_display_list(
+                        &mut self.scene,
+                        epoch,
+                        pipeline_id,
+                        background,
+                        viewport_size,
+                        list_descriptor,
+                    );
+                }
+                SceneMsg::SetRootPipeline(ref pipeline_id) => {
+                    self.scene.set_root_pipeline_id(pipeline_id.clone());
+                }
+                SceneMsg::RemovePipeline(ref pipeline_id) => {
+                    self.scene.remove_pipeline(pipeline_id);
+                }
+                _ => {}
+            }
+        }
+        for doc_msg in &txn.frame_ops {
+            match *doc_msg {
+                FrameMsg::UpdateDynamicProperties(ref properties) => {
+                    self.scene.properties.set_properties(properties);
+                }
+                _ => {}
+            }
+        }
+    }
 }
 
 impl fmt::Debug for YamlFrameWriterReceiver {
     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
         write!(f, "YamlFrameWriterReceiver")
     }
 }
 
@@ -1229,53 +1269,19 @@ impl YamlFrameWriter {
 }
 
 impl webrender::ApiRecordingReceiver for YamlFrameWriterReceiver {
     fn write_msg(&mut self, _: u32, msg: &ApiMsg) {
         match *msg {
             ApiMsg::UpdateResources(ref updates) => {
                 self.frame_writer.update_resources(updates);
             }
-            ApiMsg::UpdateDocument(_, ref txn) => {
-                self.frame_writer.update_resources(&txn.resource_updates);
-                for doc_msg in &txn.scene_ops {
-                    match *doc_msg {
-                        SceneMsg::SetDisplayList {
-                            ref epoch,
-                            ref pipeline_id,
-                            ref background,
-                            ref viewport_size,
-                            ref list_descriptor,
-                            ..
-                        } => {
-                            self.frame_writer.begin_write_display_list(
-                                &mut self.scene,
-                                epoch,
-                                pipeline_id,
-                                background,
-                                viewport_size,
-                                list_descriptor,
-                            );
-                        }
-                        SceneMsg::SetRootPipeline(ref pipeline_id) => {
-                            self.scene.set_root_pipeline_id(pipeline_id.clone());
-                        }
-                        SceneMsg::RemovePipeline(ref pipeline_id) => {
-                            self.scene.remove_pipeline(pipeline_id);
-                        }
-                        _ => {}
-                    }
-                }
-                for doc_msg in &txn.frame_ops {
-                    match *doc_msg {
-                        FrameMsg::UpdateDynamicProperties(ref properties) => {
-                            self.scene.properties.set_properties(properties);
-                        }
-                        _ => {}
-                    }
+            ApiMsg::UpdateDocuments(_, ref txns) => {
+                for txn in txns {
+                    self.update_document(txn);
                 }
             }
             _ => {}
         }
     }
 
     fn write_payload(&mut self, _frame: u32, data: &[u8]) {
         if self.frame_writer.dl_descriptor.is_some() {