Bug 1558926 - Part 1: Add data structures and pref for display item caching r=kvark
authorMiko Mynttinen <mikokm@gmail.com>
Mon, 27 Jan 2020 14:17:43 +0000
changeset 511843 8ac92137eff968a3ceeca7809f94bc51a1f97571
parent 511842 66cf77a1b464b3c739e3dd029d628930d8dcbdb7
child 511844 8c51225cdfa3e48af497a8d17655e956247abdd7
push id37060
push useraciure@mozilla.com
push dateMon, 27 Jan 2020 21:39:55 +0000
treeherdermozilla-central@df59b74d33d7 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerskvark
bugs1558926
milestone74.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 1558926 - Part 1: Add data structures and pref for display item caching r=kvark Differential Revision: https://phabricator.services.mozilla.com/D50221
gfx/layers/moz.build
gfx/layers/wr/DisplayItemCache.cpp
gfx/layers/wr/DisplayItemCache.h
gfx/webrender_bindings/src/bindings.rs
gfx/wr/webrender_api/src/display_item.rs
gfx/wr/webrender_api/src/display_item_cache.rs
gfx/wr/webrender_api/src/display_list.rs
gfx/wr/webrender_api/src/lib.rs
gfx/wr/wrench/src/rawtest.rs
gfx/wr/wrench/src/yaml_frame_reader.rs
modules/libpref/init/StaticPrefList.yaml
--- a/gfx/layers/moz.build
+++ b/gfx/layers/moz.build
@@ -251,16 +251,17 @@ EXPORTS.mozilla.layers += [
     'SyncObject.h',
     'TextureSourceProvider.h',
     'TextureWrapperImage.h',
     'TransactionIdAllocator.h',
     'TreeTraversal.h',
     'UpdateImageHelper.h',
     'wr/AsyncImagePipelineManager.h',
     'wr/ClipManager.h',
+    'wr/DisplayItemCache.h',
     'wr/IpcResourceUpdateQueue.h',
     'wr/RenderRootBoundary.h',
     'wr/RenderRootStateManager.h',
     'wr/RenderRootTypes.h',
     'wr/StackingContextHelper.h',
     'wr/WebRenderBridgeChild.h',
     'wr/WebRenderBridgeParent.h',
     'wr/WebRenderCanvasRenderer.h',
@@ -518,16 +519,17 @@ UNIFIED_SOURCES += [
     'ShareableCanvasRenderer.cpp',
     'SourceSurfaceSharedData.cpp',
     'SourceSurfaceVolatileData.cpp',
     'SyncObject.cpp',
     'TextureSourceProvider.cpp',
     'TextureWrapperImage.cpp',
     'wr/AsyncImagePipelineManager.cpp',
     'wr/ClipManager.cpp',
+    'wr/DisplayItemCache.cpp',
     'wr/IpcResourceUpdateQueue.cpp',
     'wr/RenderRootStateManager.cpp',
     'wr/RenderRootTypes.cpp',
     'wr/StackingContextHelper.cpp',
     'wr/WebRenderBridgeChild.cpp',
     'wr/WebRenderBridgeParent.cpp',
     'wr/WebRenderCanvasRenderer.cpp',
     'wr/WebRenderCommandBuilder.cpp',
new file mode 100644
--- /dev/null
+++ b/gfx/layers/wr/DisplayItemCache.cpp
@@ -0,0 +1,145 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "DisplayItemCache.h"
+
+namespace mozilla {
+namespace layers {
+
+void DisplayItemCache::UpdateState(const bool aPartialDisplayListBuildFailed,
+                                   const wr::PipelineId& aPipelineId) {
+  if (!IsEnabled()) {
+    return;
+  }
+
+  // Clear the cache if the partial display list build failed, or if the
+  // pipeline id changed.
+  const bool clearCache =
+      UpdatePipelineId(aPipelineId) || aPartialDisplayListBuildFailed;
+
+  if (clearCache) {
+    memset(mCachedItemState.Elements(), 0,
+           mCachedItemState.Length() * sizeof(CacheEntry));
+    mNextIndex = 0;
+    mFreeList.Clear();
+  }
+
+  PopulateFreeList(clearCache);
+}
+
+void DisplayItemCache::PopulateFreeList(const bool aAddAll) {
+  uint16_t index = 0;
+  for (auto& state : mCachedItemState) {
+    if (aAddAll || (!state.mUsed && state.mCached)) {
+      // This entry contained a cached item, but was not used.
+      state.mCached = false;
+      mFreeList.AppendElement(index);
+    }
+
+    state.mUsed = false;
+    index++;
+  }
+}
+
+static bool CanCacheItem(const nsDisplayItem* aItem) {
+  // Only cache leaf display items that can be reused.
+  if (!aItem->CanBeReused()) {
+    return false;
+  }
+
+  switch (aItem->GetType()) {
+    case DisplayItemType::TYPE_BACKGROUND_COLOR:
+      // case DisplayItemType::TYPE_TEXT:
+      MOZ_ASSERT(!aItem->HasChildren());
+      return true;
+    default:
+      return false;
+  }
+}
+
+void DisplayItemCache::MaybeStartCaching(nsPaintedDisplayItem* aItem,
+                                         wr::DisplayListBuilder& aBuilder) {
+  if (!IsEnabled()) {
+    return;
+  }
+
+  Stats().AddTotal();
+
+  auto& index = aItem->CacheIndex();
+  if (!index) {
+    if (!CanCacheItem(aItem)) {
+      // The item cannot be cached.
+      return;
+    }
+
+    index = GetNextCacheIndex();
+    if (!index) {
+      // The item does not fit in the cache.
+      return;
+    }
+  }
+
+  // Update the current cache index, which is used by |MaybeEndCaching()| below.
+  MOZ_ASSERT(!mCurrentIndex);
+  mCurrentIndex = index;
+
+  MOZ_ASSERT(CanCacheItem(aItem));
+  MOZ_ASSERT(mCurrentIndex && CurrentCacheSize() > *mCurrentIndex);
+
+  auto& state = mCachedItemState[*mCurrentIndex];
+  MOZ_ASSERT(!state.mCached);
+  state.mCached = true;
+  MOZ_ASSERT(!state.mUsed);
+  state.mUsed = true;
+  state.mSpaceAndClip = aBuilder.CurrentSpaceAndClipChain();
+
+  Stats().AddCached();
+  aBuilder.StartCachedItem(*mCurrentIndex);
+}
+
+void DisplayItemCache::MaybeEndCaching(wr::DisplayListBuilder& aBuilder) {
+  if (IsEnabled() && mCurrentIndex) {
+    aBuilder.EndCachedItem(*mCurrentIndex);
+    mCurrentIndex = Nothing();
+  }
+}
+
+bool DisplayItemCache::ReuseItem(nsPaintedDisplayItem* aItem,
+                                 wr::DisplayListBuilder& aBuilder) {
+  if (!IsEnabled()) {
+    return false;
+  }
+
+  auto& index = aItem->CacheIndex();
+  if (!index) {
+    return false;
+  }
+
+  auto& state = mCachedItemState[*index];
+  if (!state.mCached) {
+    // The display item has a stale cache state.
+    return false;  // Recache item.
+  }
+
+  // Spatial id and clip id can change between display lists.
+  if (!(aBuilder.CurrentSpaceAndClipChain() == state.mSpaceAndClip)) {
+    // TODO(miko): Technically we might be able to update just the changed data
+    // here but it adds a lot of complexity.
+    // Mark the cache state false and recache the item.
+    state.mCached = false;
+    return false;
+  }
+
+  Stats().AddReused();
+  Stats().AddTotal();
+
+  state.mUsed = true;
+  aBuilder.ReuseItem(*index);
+  return true;
+}
+
+}  // namespace layers
+}  // namespace mozilla
new file mode 100644
--- /dev/null
+++ b/gfx/layers/wr/DisplayItemCache.h
@@ -0,0 +1,158 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=8 sts=2 et sw=2 tw=80: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef GFX_DISPLAY_ITEM_CACHE_H
+#define GFX_DISPLAY_ITEM_CACHE_H
+
+#include "mozilla/webrender/WebRenderAPI.h"
+#include "mozilla/Maybe.h"
+#include "nsTArray.h"
+
+class nsPaintedDisplayItem;
+
+namespace mozilla {
+
+namespace wr {
+class DisplayListBuilder;
+}  // namespace wr
+
+namespace layers {
+
+class CacheStats {
+ public:
+  CacheStats() = default;
+
+  void Reset() { mCached = mReused = mTotal = 0; }
+
+  void Print() {
+    printf("Cached: %zu, Reused: %zu, Total: %zu\n", mCached, mReused, mTotal);
+  }
+
+  void AddCached() { mCached++; }
+  void AddReused() { mReused++; }
+  void AddTotal() { mTotal++; }
+
+ private:
+  size_t mCached = 0;
+  size_t mReused = 0;
+  size_t mTotal = 0;
+};
+
+/**
+ * DisplayItemCache keeps track of which Gecko display items have already had
+ * their respective WebRender display items sent to WebRender backend.
+ *
+ * Ideally creating the WR display items for a Gecko display item would not
+ * depend on any external state. However currently pipeline id, clip id, and
+ * spatial id can change between display lists, even if the Gecko display items
+ * have not. This state is tracked by DisplayItemCache.
+ */
+class DisplayItemCache final {
+ public:
+  DisplayItemCache() : mMaxCacheSize(0), mNextIndex(0) {}
+
+  bool IsEnabled() const { return mMaxCacheSize > 0; }
+
+  /**
+   * Updates the cache state based on the given display list build information
+   * and pipeline id.
+   *
+   * This is necessary because Gecko display items can only be reused for the
+   * partial display list builds following a full display list build.
+   */
+  void UpdateState(const bool aPartialDisplayListBuildFailed,
+                   const wr::PipelineId& aPipelineId);
+
+  /**
+   * Returns the current cache size.
+   */
+  size_t CurrentCacheSize() const {
+    return IsEnabled() ? mCachedItemState.Length() : 0;
+  }
+
+  /**
+   * Sets the initial and max cache size to given |aInitialSize| and |aMaxSize|.
+   *
+   * Currently the cache size is constant, but a good improvement would be to
+   * set the initial and maximum size based on the display list length.
+   */
+  void SetCapacity(const size_t aInitialSize, const size_t aMaxSize) {
+    mMaxCacheSize = aMaxSize;
+    mCachedItemState.SetCapacity(aMaxSize);
+    mCachedItemState.SetLength(aInitialSize);
+    mFreeList.SetCapacity(aMaxSize);
+  }
+
+  /**
+   * If the given display item |aItem| can be cached, update the cache state of
+   * the item and tell WR DisplayListBuilder |aBuilder| to cache WR display
+   * items until |EndCaching()| is called.
+   *
+   * If the display item cannot be cached, this function does nothing.
+   */
+  void MaybeStartCaching(nsPaintedDisplayItem* aItem,
+                         wr::DisplayListBuilder& aBuilder);
+
+  /**
+   * Tell WR DisplayListBuilder |aBuilder| to stop caching WR display items.
+   *
+   * If the display item cannot be cached, this function does nothing.
+   */
+  void MaybeEndCaching(wr::DisplayListBuilder& aBuilder);
+
+  /**
+   * If the given |aItem| has been cached, tell WR DisplayListBuilder |aBuilder|
+   * to reuse it.
+   * Returns true if the item was reused, otherwise returns false.
+   */
+  bool ReuseItem(nsPaintedDisplayItem* aItem, wr::DisplayListBuilder& aBuilder);
+
+  CacheStats& Stats() { return mCacheStats; }
+
+ private:
+  struct CacheEntry {
+    wr::WrSpaceAndClipChain mSpaceAndClip;
+    bool mCached;
+    bool mUsed;
+  };
+
+  Maybe<uint16_t> GetNextCacheIndex() {
+    if (mFreeList.IsEmpty()) {
+      return Nothing();
+    }
+
+    return Some(mFreeList.PopLastElement());
+  }
+
+  /**
+   * Iterates through |mCachedItemState| and adds unused entries to free list.
+   * If |aAddAll| is true, adds every entry regardless of the state.
+   */
+  void PopulateFreeList(const bool aAddAll);
+
+  /**
+   * Returns true if the given |aPipelineId| is different from the previous one,
+   * otherwise returns false.
+   */
+  bool UpdatePipelineId(const wr::PipelineId& aPipelineId) {
+    const bool isSame = mPreviousPipelineId.refOr(aPipelineId) == aPipelineId;
+    mPreviousPipelineId = Some(aPipelineId);
+    return !isSame;
+  }
+
+  nsTArray<CacheEntry> mCachedItemState;
+  nsTArray<uint16_t> mFreeList;
+  size_t mMaxCacheSize;
+  uint16_t mNextIndex;
+  Maybe<uint16_t> mCurrentIndex;
+  Maybe<wr::PipelineId> mPreviousPipelineId;
+  CacheStats mCacheStats;
+};
+
+}  // namespace layers
+}  // namespace mozilla
+
+#endif /* GFX_DISPLAY_ITEM_CACHE_H */
--- a/gfx/webrender_bindings/src/bindings.rs
+++ b/gfx/webrender_bindings/src/bindings.rs
@@ -2287,30 +2287,32 @@ impl WebRenderFrameBuilder {
     }
 
 }
 
 pub struct WrState {
     pipeline_id: WrPipelineId,
     frame_builder: WebRenderFrameBuilder,
     current_tag: Option<ItemTag>,
+    current_item_key: Option<ItemKey>,
 }
 
 #[no_mangle]
 pub extern "C" fn wr_state_new(pipeline_id: WrPipelineId,
                                content_size: LayoutSize,
                                capacity: usize) -> *mut WrState {
     assert!(unsafe { !is_in_render_thread() });
 
     let state = Box::new(WrState {
                              pipeline_id: pipeline_id,
                              frame_builder: WebRenderFrameBuilder::with_capacity(pipeline_id,
                                                                                  content_size,
                                                                                  capacity),
                              current_tag: None,
+                             current_item_key: None,
                          });
 
     Box::into_raw(state)
 }
 
 #[no_mangle]
 pub extern "C" fn wr_state_delete(state: *mut WrState) {
     assert!(unsafe { !is_in_render_thread() });
@@ -2671,16 +2673,17 @@ pub extern "C" fn wr_dp_push_rect(state:
         // NB: the damp-e10s talos-test will frequently crash on startup if we
         // early-return here for empty rects. I couldn't figure out why, but
         // it's pretty harmless to feed these through, so, uh, we do?
         clip_rect: clip_rect.unwrap_or(LayoutRect::zero()),
         clip_id: space_and_clip.clip_id,
         spatial_id: space_and_clip.spatial_id,
         flags: prim_flags(is_backface_visible),
         hit_info: state.current_tag,
+        item_key: state.current_item_key,
     };
 
     state.frame_builder.dl_builder.push_rect(
         &prim_info,
         color,
     );
 }
 
@@ -2701,16 +2704,17 @@ pub extern "C" fn wr_dp_push_rect_with_p
     if clip_rect.is_none() { return; }
 
     let prim_info = CommonItemProperties {
         clip_rect: clip_rect.unwrap(),
         clip_id: space_and_clip.clip_id,
         spatial_id: space_and_clip.spatial_id,
         flags: prim_flags(is_backface_visible),
         hit_info: state.current_tag,
+        item_key: state.current_item_key,
     };
 
     state.frame_builder.dl_builder.push_rect(
         &prim_info,
         color,
     );
 }
 
@@ -2753,16 +2757,17 @@ pub extern "C" fn wr_dp_push_backdrop_fi
     if clip_rect.is_none() { return; }
 
     let prim_info = CommonItemProperties {
         clip_rect: clip_rect.unwrap(),
         clip_id: space_and_clip.clip_id,
         spatial_id: space_and_clip.spatial_id,
         flags: prim_flags(is_backface_visible),
         hit_info: state.current_tag,
+        item_key: state.current_item_key,
     };
 
     state.frame_builder.dl_builder.push_backdrop_filter(
         &prim_info,
         &filters,
         &filter_datas,
         &[],
     );
@@ -2781,16 +2786,17 @@ pub extern "C" fn wr_dp_push_clear_rect(
     if clip_rect.is_none() { return; }
 
     let prim_info = CommonItemProperties {
         clip_rect: clip_rect.unwrap(),
         clip_id: space_and_clip.clip_id,
         spatial_id: space_and_clip.spatial_id,
         flags: prim_flags(true),
         hit_info: state.current_tag,
+        item_key: state.current_item_key,
     };
 
     state.frame_builder.dl_builder.push_clear_rect(
         &prim_info,
     );
 }
 
 #[no_mangle]
@@ -2807,16 +2813,17 @@ pub extern "C" fn wr_dp_push_hit_test(st
     if clip_rect.is_none() { return; }
 
     let prim_info = CommonItemProperties {
         clip_rect: clip_rect.unwrap(),
         clip_id: space_and_clip.clip_id,
         spatial_id: space_and_clip.spatial_id,
         flags: prim_flags(is_backface_visible),
         hit_info: state.current_tag,
+        item_key: state.current_item_key,
     };
 
     state.frame_builder.dl_builder.push_hit_test(
         &prim_info,
     );
 }
 
 #[no_mangle]
@@ -2834,16 +2841,17 @@ pub extern "C" fn wr_dp_push_clear_rect_
     if clip_rect.is_none() { return; }
 
     let prim_info = CommonItemProperties {
         clip_rect: clip_rect.unwrap(),
         clip_id: space_and_clip.clip_id,
         spatial_id: space_and_clip.spatial_id,
         flags: prim_flags(true),
         hit_info: state.current_tag,
+        item_key: state.current_item_key,
     };
 
     state.frame_builder.dl_builder.push_clear_rect(
         &prim_info,
     );
 }
 
 #[no_mangle]
@@ -2861,16 +2869,17 @@ pub extern "C" fn wr_dp_push_image(state
     let space_and_clip = parent.to_webrender(state.pipeline_id);
 
     let prim_info = CommonItemProperties {
         clip_rect: clip,
         clip_id: space_and_clip.clip_id,
         spatial_id: space_and_clip.spatial_id,
         flags: prim_flags(is_backface_visible),
         hit_info: state.current_tag,
+        item_key: state.current_item_key,
     };
 
     let alpha_type = if premultiplied_alpha {
         AlphaType::PremultipliedAlpha
     } else {
         AlphaType::Alpha
     };
 
@@ -2901,16 +2910,17 @@ pub extern "C" fn wr_dp_push_repeating_i
     let space_and_clip = parent.to_webrender(state.pipeline_id);
 
     let prim_info = CommonItemProperties {
         clip_rect: clip,
         clip_id: space_and_clip.clip_id,
         spatial_id: space_and_clip.spatial_id,
         flags: prim_flags(is_backface_visible),
         hit_info: state.current_tag,
+        item_key: state.current_item_key,
     };
 
     let alpha_type = if premultiplied_alpha {
         AlphaType::PremultipliedAlpha
     } else {
         AlphaType::Alpha
     };
 
@@ -2945,16 +2955,17 @@ pub extern "C" fn wr_dp_push_yuv_planar_
     let space_and_clip = parent.to_webrender(state.pipeline_id);
 
     let prim_info = CommonItemProperties {
         clip_rect: clip,
         clip_id: space_and_clip.clip_id,
         spatial_id: space_and_clip.spatial_id,
         flags: prim_flags(is_backface_visible),
         hit_info: state.current_tag,
+        item_key: state.current_item_key,
     };
 
     state.frame_builder
          .dl_builder
          .push_yuv_image(&prim_info,
                          bounds,
                          YuvData::PlanarYCbCr(image_key_0, image_key_1, image_key_2),
                          color_depth,
@@ -2981,16 +2992,17 @@ pub extern "C" fn wr_dp_push_yuv_NV12_im
     let space_and_clip = parent.to_webrender(state.pipeline_id);
 
     let prim_info = CommonItemProperties {
         clip_rect: clip,
         clip_id: space_and_clip.clip_id,
         spatial_id: space_and_clip.spatial_id,
         flags: prim_flags(is_backface_visible),
         hit_info: state.current_tag,
+        item_key: state.current_item_key,
     };
 
     state.frame_builder
          .dl_builder
          .push_yuv_image(&prim_info,
                          bounds,
                          YuvData::NV12(image_key_0, image_key_1),
                          color_depth,
@@ -3016,16 +3028,17 @@ pub extern "C" fn wr_dp_push_yuv_interle
     let space_and_clip = parent.to_webrender(state.pipeline_id);
 
     let prim_info = CommonItemProperties {
         clip_rect: clip,
         clip_id: space_and_clip.clip_id,
         spatial_id: space_and_clip.spatial_id,
         flags: prim_flags(is_backface_visible),
         hit_info: state.current_tag,
+        item_key: state.current_item_key,
     };
 
     state.frame_builder
          .dl_builder
          .push_yuv_image(&prim_info,
                          bounds,
                          YuvData::InterleavedYCbCr(image_key_0),
                          color_depth,
@@ -3051,17 +3064,18 @@ pub extern "C" fn wr_dp_push_text(state:
 
     let space_and_clip = parent.to_webrender(state.pipeline_id);
 
     let prim_info = CommonItemProperties {
         clip_rect: clip,
         spatial_id: space_and_clip.spatial_id,
         clip_id: space_and_clip.clip_id,
         flags: prim_flags(is_backface_visible),
-        hit_info: state.current_tag
+        hit_info: state.current_tag,
+        item_key: state.current_item_key,
     };
 
     state.frame_builder
          .dl_builder
          .push_text(&prim_info,
                     bounds,
                     &glyph_slice,
                     font_key,
@@ -3108,16 +3122,17 @@ pub extern "C" fn wr_dp_push_line(state:
     let space_and_clip = parent.to_webrender(state.pipeline_id);
 
     let prim_info = CommonItemProperties {
         clip_rect: *clip,
         clip_id: space_and_clip.clip_id,
         spatial_id: space_and_clip.spatial_id,
         flags: prim_flags(is_backface_visible),
         hit_info: state.current_tag,
+        item_key: state.current_item_key,
     };
 
     state.frame_builder
          .dl_builder
          .push_line(&prim_info,
                     bounds,
                     wavy_line_thickness,
                     orientation,
@@ -3153,16 +3168,17 @@ pub extern "C" fn wr_dp_push_border(stat
     let space_and_clip = parent.to_webrender(state.pipeline_id);
 
     let prim_info = CommonItemProperties {
         clip_rect: clip,
         clip_id: space_and_clip.clip_id,
         spatial_id: space_and_clip.spatial_id,
         flags: prim_flags(is_backface_visible),
         hit_info: state.current_tag,
+        item_key: state.current_item_key,
     };
 
     state.frame_builder
          .dl_builder
          .push_border(&prim_info,
                       rect,
                       widths,
                       border_details);
@@ -3202,16 +3218,17 @@ pub extern "C" fn wr_dp_push_border_imag
     let space_and_clip = parent.to_webrender(state.pipeline_id);
 
     let prim_info = CommonItemProperties {
         clip_rect: clip,
         clip_id: space_and_clip.clip_id,
         spatial_id: space_and_clip.spatial_id,
         flags: prim_flags(is_backface_visible),
         hit_info: state.current_tag,
+        item_key: state.current_item_key,
     };
 
     state.frame_builder.dl_builder.push_border(
         &prim_info,
         rect,
         params.widths,
         border_details,
     );
@@ -3260,16 +3277,17 @@ pub extern "C" fn wr_dp_push_border_grad
     let space_and_clip = parent.to_webrender(state.pipeline_id);
 
     let prim_info = CommonItemProperties {
         clip_rect: clip,
         clip_id: space_and_clip.clip_id,
         spatial_id: space_and_clip.spatial_id,
         flags: prim_flags(is_backface_visible),
         hit_info: state.current_tag,
+        item_key: state.current_item_key,
     };
 
     state.frame_builder.dl_builder.push_border(
         &prim_info,
         rect,
         widths.into(),
         border_details,
     );
@@ -3322,16 +3340,17 @@ pub extern "C" fn wr_dp_push_border_radi
     let space_and_clip = parent.to_webrender(state.pipeline_id);
 
     let prim_info = CommonItemProperties {
         clip_rect: clip,
         clip_id: space_and_clip.clip_id,
         spatial_id: space_and_clip.spatial_id,
         flags: prim_flags(is_backface_visible),
         hit_info: state.current_tag,
+        item_key: state.current_item_key,
     };
 
     state.frame_builder.dl_builder.push_border(
         &prim_info,
         rect,
         widths.into(),
         border_details,
     );
@@ -3365,16 +3384,17 @@ pub extern "C" fn wr_dp_push_linear_grad
     let space_and_clip = parent.to_webrender(state.pipeline_id);
 
     let prim_info = CommonItemProperties {
         clip_rect: clip,
         clip_id: space_and_clip.clip_id,
         spatial_id: space_and_clip.spatial_id,
         flags: prim_flags(is_backface_visible),
         hit_info: state.current_tag,
+        item_key: state.current_item_key,
     };
 
     state.frame_builder.dl_builder.push_gradient(
         &prim_info,
         rect,
         gradient,
         tile_size.into(),
         tile_spacing.into(),
@@ -3409,16 +3429,17 @@ pub extern "C" fn wr_dp_push_radial_grad
     let space_and_clip = parent.to_webrender(state.pipeline_id);
 
     let prim_info = CommonItemProperties {
         clip_rect: clip,
         clip_id: space_and_clip.clip_id,
         spatial_id: space_and_clip.spatial_id,
         flags: prim_flags(is_backface_visible),
         hit_info: state.current_tag,
+        item_key: state.current_item_key,
     };
 
     state.frame_builder.dl_builder.push_radial_gradient(
         &prim_info,
         rect,
         gradient,
         tile_size,
         tile_spacing);
@@ -3442,16 +3463,17 @@ pub extern "C" fn wr_dp_push_box_shadow(
     let space_and_clip = parent.to_webrender(state.pipeline_id);
 
     let prim_info = CommonItemProperties {
         clip_rect: clip,
         clip_id: space_and_clip.clip_id,
         spatial_id: space_and_clip.spatial_id,
         flags: prim_flags(is_backface_visible),
         hit_info: state.current_tag,
+        item_key: state.current_item_key,
     };
 
     state.frame_builder
          .dl_builder
          .push_box_shadow(&prim_info,
                           box_bounds,
                           offset,
                           color,
--- a/gfx/wr/webrender_api/src/display_item.rs
+++ b/gfx/wr/webrender_api/src/display_item.rs
@@ -28,16 +28,20 @@ pub const MAX_BLUR_RADIUS: f32 = 300.;
 /// is missing then the item doesn't take part in hit testing at all. This
 /// is composed of two numbers. In Servo, the first is an identifier while the
 /// second is used to select the cursor that should be used during mouse
 /// movement. In Gecko, the first is a scrollframe identifier, while the second
 /// is used to store various flags that APZ needs to properly process input
 /// events.
 pub type ItemTag = (u64, u16);
 
+/// An identifier used to refer to previously sent display items. Currently it
+/// refers to individual display items, but this may change later.
+pub type ItemKey = u16;
+
 bitflags! {
     #[repr(C)]
     #[derive(Deserialize, MallocSizeOf, Serialize, PeekPoke)]
     pub struct PrimitiveFlags: u8 {
         /// The CSS backface-visibility property (yes, it can be really granular)
         const IS_BACKFACE_VISIBLE = 1 << 0;
         /// If set, this primitive represents a scroll bar container
         const IS_SCROLLBAR_CONTAINER = 1 << 1;
@@ -69,30 +73,33 @@ pub struct CommonItemProperties {
     /// The coordinate-space the item is in (yes, it can be really granular)
     pub spatial_id: SpatialId,
     /// Opaque bits for our clients to use for hit-testing. This is the most
     /// dubious "common" field, but because it's an Option, it usually only
     /// wastes a single byte (for None).
     pub hit_info: Option<ItemTag>,
     /// Various flags describing properties of this primitive.
     pub flags: PrimitiveFlags,
+    /// The unique id of this display item.
+    pub item_key: Option<ItemKey>
 }
 
 impl CommonItemProperties {
     /// Convenience for tests.
     pub fn new(
         clip_rect: LayoutRect,
         space_and_clip: SpaceAndClipInfo,
     ) -> Self {
         Self {
             clip_rect,
             spatial_id: space_and_clip.spatial_id,
             clip_id: space_and_clip.clip_id,
             hit_info: None,
             flags: PrimitiveFlags::default(),
+            item_key: None,
         }
     }
 }
 
 /// Per-primitive information about the nodes in the clip tree and
 /// the spatial tree that the primitive belongs to.
 ///
 /// Note: this is a separate struct from `PrimitiveInfo` because
new file mode 100644
--- /dev/null
+++ b/gfx/wr/webrender_api/src/display_item_cache.rs
@@ -0,0 +1,106 @@
+/* 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 crate::display_item::*;
+use crate::display_list::*;
+
+#[derive(Clone, Debug, Deserialize, PartialEq, Serialize)]
+pub struct CachedDisplayItem {
+    item: DisplayItem,
+    data: Vec<u8>,
+}
+
+impl CachedDisplayItem {
+    pub fn item(&self) -> &DisplayItem {
+        &self.item
+    }
+
+    pub fn data_as_item_range<T>(&self) -> ItemRange<T> {
+        ItemRange::new(&self.data)
+    }
+}
+
+impl From<DisplayItemRef<'_, '_>> for CachedDisplayItem {
+    fn from(item_ref: DisplayItemRef) -> Self {
+        let item = item_ref.item();
+
+        match item {
+            DisplayItem::Text(..) => CachedDisplayItem {
+                item: *item,
+                data: item_ref.glyphs().bytes().to_vec(),
+            },
+            DisplayItem::Rectangle(..) |
+            DisplayItem::Image(..) => CachedDisplayItem {
+                item: *item,
+                data: Vec::new(),
+            },
+            _ => { unimplemented!("Unsupported display item type"); }
+        }
+    }
+}
+
+#[derive(Clone, Default, Deserialize, Serialize)]
+pub struct DisplayItemCache {
+    items: Vec<Option<CachedDisplayItem>>
+}
+
+impl DisplayItemCache {
+    fn grow_if_needed(
+        &mut self,
+        capacity: usize
+    ) {
+        if capacity > self.items.len() {
+            self.items.resize_with(capacity, || None::<CachedDisplayItem>);
+            // println!("Current cache size: {:?}",
+            //     mem::size_of::<CachedDisplayItem>() * capacity);
+        }
+    }
+
+    pub fn add_item(
+        &mut self,
+        key: Option<ItemKey>,
+        item: DisplayItemRef
+    ) {
+        let index = usize::from(key.expect("Cached item without key"));
+        self.items[index] = Some(CachedDisplayItem::from(item));
+    }
+
+    pub fn get_item(
+        &self,
+        key: ItemKey
+    ) -> Option<&CachedDisplayItem> {
+        self.items[key as usize].as_ref()
+    }
+
+    pub fn update(
+        &mut self,
+        display_list: &BuiltDisplayList
+    ) {
+        self.grow_if_needed(display_list.cache_size());
+
+        let mut iter = display_list.extra_data_iter();
+
+        loop {
+            let item = match iter.next() {
+                Some(item) => item,
+                None => break,
+            };
+
+            match item.item() {
+                DisplayItem::Rectangle(ref info) => {
+                    self.add_item(info.common.item_key, item);
+                }
+                DisplayItem::Text(ref info) => {
+                    self.add_item(info.common.item_key, item);
+                }
+                DisplayItem::Image(ref info) => {
+                    self.add_item(info.common.item_key, item);
+                }
+                item @ _ => {
+                    unimplemented!("Unexpected item in extra data: {:?}", item);
+                }
+            }
+        }
+    }
+}
--- a/gfx/wr/webrender_api/src/display_list.rs
+++ b/gfx/wr/webrender_api/src/display_list.rs
@@ -57,20 +57,31 @@ impl<'a, T> Default for ItemRange<'a, T>
         ItemRange {
             bytes: Default::default(),
             _boo: PhantomData,
         }
     }
 }
 
 impl<'a, T> ItemRange<'a, T> {
+    pub fn new(bytes: &'a [u8]) -> Self {
+        Self {
+            bytes,
+            _boo: PhantomData
+        }
+    }
+
     pub fn is_empty(&self) -> bool {
         // Nothing more than space for a length (0).
         self.bytes.len() <= mem::size_of::<usize>()
     }
+
+    pub fn bytes(&self) -> &[u8] {
+        &self.bytes
+    }
 }
 
 impl<'a, T: Default> ItemRange<'a, T> {
     pub fn iter(&self) -> AuxIter<'a, T> {
         AuxIter::new(T::default(), self.bytes)
     }
 }
 
--- a/gfx/wr/webrender_api/src/lib.rs
+++ b/gfx/wr/webrender_api/src/lib.rs
@@ -39,21 +39,23 @@ extern crate time;
 
 extern crate malloc_size_of;
 extern crate peek_poke;
 
 mod api;
 pub mod channel;
 mod color;
 mod display_item;
+mod display_item_cache;
 mod display_list;
 mod font;
 mod gradient_builder;
 mod image;
 pub mod units;
 
 pub use crate::api::*;
 pub use crate::color::*;
 pub use crate::display_item::*;
+pub use crate::display_item_cache::DisplayItemCache;
 pub use crate::display_list::*;
 pub use crate::font::*;
 pub use crate::gradient_builder::*;
 pub use crate::image::*;
--- a/gfx/wr/wrench/src/rawtest.rs
+++ b/gfx/wr/wrench/src/rawtest.rs
@@ -116,31 +116,33 @@ impl<'a> RawtestHarness<'a> {
     fn make_common_properties(&self, clip_rect: LayoutRect) -> CommonItemProperties {
         let space_and_clip = SpaceAndClipInfo::root_scroll(self.wrench.root_pipeline_id);
         CommonItemProperties {
             clip_rect,
             clip_id: space_and_clip.clip_id,
             spatial_id: space_and_clip.spatial_id,
             flags: PrimitiveFlags::default(),
             hit_info: None,
+            item_key: None,
         }
     }
 
     fn make_common_properties_with_clip_and_spatial(
         &self,
         clip_rect: LayoutRect,
         clip_id: ClipId,
         spatial_id: SpatialId
     ) -> CommonItemProperties {
         CommonItemProperties {
             clip_rect,
             clip_id,
             spatial_id,
             flags: PrimitiveFlags::default(),
             hit_info: None,
+            item_key: None,
         }
     }
 
     fn test_resize_image(&mut self) {
         println!("\tresize image...");
         // This test changes the size of an image to make it go switch back and forth
         // between tiled and non-tiled.
         // The resource cache should be able to handle this without crashing.
@@ -320,16 +322,17 @@ impl<'a> RawtestHarness<'a> {
         );
 
         let info = CommonItemProperties {
             clip_rect: rect(0.0, 0.0, 800.0, 800.0),
             clip_id,
             spatial_id: root_space_and_clip.spatial_id,
             flags: PrimitiveFlags::default(),
             hit_info: None,
+            item_key: None,
         };
 
         // setup some malicious image size parameters
         builder.push_repeating_image(
             &info,
             size2(15000.0, 15000.0).into(),
             size2(15000.0, 15000.0),
             size2(0.0, 0.0),
@@ -408,16 +411,17 @@ impl<'a> RawtestHarness<'a> {
         );
 
         let info = CommonItemProperties {
             clip_rect: rect(10.0, 10.0, 400.0, 400.0),
             clip_id,
             spatial_id: root_space_and_clip.spatial_id,
             flags: PrimitiveFlags::default(),
             hit_info: None,
+            item_key: None,
         };
 
         builder.push_repeating_image(
             &info,
             info.clip_rect,
             image_size,
             image_size,
             ImageRendering::Auto,
@@ -505,16 +509,17 @@ impl<'a> RawtestHarness<'a> {
         );
 
         let info = CommonItemProperties {
             clip_rect: rect(0.0, 0.0, 1000.0, 1000.0),
             clip_id,
             spatial_id: root_space_and_clip.spatial_id,
             flags: PrimitiveFlags::default(),
             hit_info: None,
+            item_key: None,
         };
 
         builder.push_repeating_image(
             &info,
             rect(0.0, 0.0, 500.0, 500.0),
             size2(500.0, 500.0),
             size2(500.0, 500.0),
             ImageRendering::Auto,
@@ -552,16 +557,17 @@ impl<'a> RawtestHarness<'a> {
         );
 
         let info = CommonItemProperties {
             clip_rect: rect(0.0, 0.0, 1000.0, 1000.0),
             clip_id,
             spatial_id: root_space_and_clip.spatial_id,
             flags: PrimitiveFlags::default(),
             hit_info: None,
+            item_key: None,
         };
 
         builder.push_repeating_image(
             &info,
             rect(50.0, 50.0, 400.0, 400.0),
             size2(400.0, 400.0),
             size2(400.0, 400.0),
             ImageRendering::Auto,
@@ -601,16 +607,17 @@ impl<'a> RawtestHarness<'a> {
         );
 
         let info = CommonItemProperties {
             clip_rect: rect(0.0, 0.0, 1000.0, 1000.0),
             clip_id,
             spatial_id: root_space_and_clip.spatial_id,
             flags: PrimitiveFlags::default(),
             hit_info: None,
+            item_key: None,
         };
 
         builder.push_repeating_image(
             &info,
             rect(50.0, 50.0, 400.0, 400.0),
             size2(400.0, 400.0),
             size2(400.0, 400.0),
             ImageRendering::Auto,
@@ -1119,16 +1126,17 @@ impl<'a> RawtestHarness<'a> {
                     true,
                 );
                 let info = CommonItemProperties {
                     clip_rect: rect(110., 110., 50., 2.),
                     clip_id,
                     spatial_id,
                     flags: PrimitiveFlags::default(),
                     hit_info: None,
+                    item_key: None,
                 };
                 builder.push_line(
                     &info,
                     &info.clip_rect,
                     0.0, LineOrientation::Horizontal,
                     &ColorF::new(0.0, 0.0, 0.0, 1.0),
                     LineStyle::Solid,
                 );
@@ -1369,16 +1377,17 @@ impl<'a> RawtestHarness<'a> {
             &space_and_clip,
             rect,
             vec![make_rounded_complex_clip(&rect, 20.)],
             None,
         );
         builder.push_rect(
             &CommonItemProperties {
                 hit_info: Some((0, 4)),
+                item_key: None,
                 clip_rect: rect,
                 clip_id: temp_clip_id,
                 spatial_id: space_and_clip.spatial_id,
                 flags: PrimitiveFlags::default(),
             },
             ColorF::new(1.0, 1.0, 1.0, 1.0),
         );
 
@@ -1389,16 +1398,17 @@ impl<'a> RawtestHarness<'a> {
             rect,
             vec![make_rounded_complex_clip(&rect, 20.)],
             None,
         );
         let clip_chain_id = builder.define_clip_chain(None, vec![clip_id]);
         builder.push_rect(
             &CommonItemProperties {
                 hit_info: Some((0, 5)),
+                item_key: None,
                 clip_rect: rect,
                 clip_id: ClipId::ClipChain(clip_chain_id),
                 spatial_id: space_and_clip.spatial_id,
                 flags: PrimitiveFlags::default(),
             },
             ColorF::new(1.0, 1.0, 1.0, 1.0),
         );
 
--- a/gfx/wr/wrench/src/yaml_frame_reader.rs
+++ b/gfx/wr/wrench/src/yaml_frame_reader.rs
@@ -470,16 +470,17 @@ impl YamlFrameReader {
         let content_size = self.get_root_size_from_yaml(wrench, yaml);
         let mut builder = DisplayListBuilder::new(pipeline_id, content_size);
         let mut info = CommonItemProperties {
             clip_rect: LayoutRect::zero(),
             clip_id: ClipId::invalid(),
             spatial_id: SpatialId::new(0, PipelineId::dummy()),
             flags: PrimitiveFlags::default(),
             hit_info: None,
+            item_key: None,
         };
         self.add_stacking_context_from_yaml(&mut builder, wrench, yaml, true, &mut info);
         self.display_lists.push(builder.finalize());
 
         assert_eq!(self.clip_id_stack.len(), 1);
         assert_eq!(self.spatial_id_stack.len(), 1);
     }
 
@@ -1674,16 +1675,17 @@ impl YamlFrameReader {
                 }
             }
 
             let mut info = CommonItemProperties {
                 clip_rect,
                 clip_id: space_and_clip.clip_id,
                 spatial_id: space_and_clip.spatial_id,
                 hit_info: self.to_hit_testing_tag(&item["hit-testing-tag"]),
+                item_key: None,
                 flags,
             };
 
             match item_type {
                 "rect" => self.handle_rect(dl, item, &mut info),
                 "hit-test" => self.handle_hit_test(dl, item, &mut info),
                 "clear-rect" => self.handle_clear_rect(dl, item, &mut info),
                 "line" => self.handle_line(dl, item, &mut info),
--- a/modules/libpref/init/StaticPrefList.yaml
+++ b/modules/libpref/init/StaticPrefList.yaml
@@ -3863,16 +3863,21 @@
   type: bool
 #ifdef DEBUG
   value: true
 #else
   value: false
 #endif
   mirror: once
 
+- name: gfx.webrender.enable-item-cache
+  type: bool
+  value: false
+  mirror: once
+
 #ifdef NIGHTLY_BUILD
   # Keep this pref hidden on non-nightly builds to avoid people accidentally
   # turning it on.
 - name: gfx.webrender.panic-on-gl-error
   type: bool
   value: false
   mirror: once
 #endif