Fix child caching in opacity_layer (#17914)

Choose a child more likely to remain stable from frame to frame as the target to cache in the OpacityLayer.
diff --git a/flow/layers/opacity_layer.cc b/flow/layers/opacity_layer.cc
index d864db3..9fd0bcc 100644
--- a/flow/layers/opacity_layer.cc
+++ b/flow/layers/opacity_layer.cc
@@ -55,7 +55,7 @@
 #ifndef SUPPORT_FRACTIONAL_TRANSLATION
     child_matrix = RasterCache::GetIntegralTransCTM(child_matrix);
 #endif
-    TryToPrepareRasterCache(context, container, child_matrix);
+    TryToPrepareRasterCache(context, GetCacheableChild(), child_matrix);
   }
 
   // Restore cull_rect
@@ -78,7 +78,7 @@
 #endif
 
   if (context.raster_cache &&
-      context.raster_cache->Draw(GetChildContainer(),
+      context.raster_cache->Draw(GetCacheableChild(),
                                  *context.leaf_nodes_canvas, &paint)) {
     return;
   }
@@ -118,4 +118,13 @@
   return static_cast<ContainerLayer*>(layers()[0].get());
 }
 
+Layer* OpacityLayer::GetCacheableChild() const {
+  ContainerLayer* child_container = GetChildContainer();
+  if (child_container->layers().size() == 1) {
+    return child_container->layers()[0].get();
+  }
+
+  return child_container;
+}
+
 }  // namespace flutter
diff --git a/flow/layers/opacity_layer.h b/flow/layers/opacity_layer.h
index 658dd1a..4edc61a 100644
--- a/flow/layers/opacity_layer.h
+++ b/flow/layers/opacity_layer.h
@@ -38,8 +38,58 @@
 #endif  // defined(OS_FUCHSIA)
 
  private:
+  /**
+   * @brief Returns the ContainerLayer used to hold all of the children
+   * of the OpacityLayer.
+   *
+   * Often opacity layers will only have a single child since the associated
+   * Flutter widget is specified with only a single child widget pointer.
+   * But depending on the structure of the child tree that single widget at
+   * the framework level can turn into multiple children at the engine
+   * API level since there is no guarantee of a 1:1 correspondence of widgets
+   * to engine layers. This synthetic child container layer is established to
+   * hold all of the children in a single layer so that we can cache their
+   * output, but this synthetic layer will typically not be the best choice
+   * for the layer cache since the synthetic container is created fresh with
+   * each new OpacityLayer, and so may not be stable from frame to frame.
+   *
+   * @see GetCacheableChild()
+   * @return the ContainerLayer child used to hold the children
+   */
   ContainerLayer* GetChildContainer() const;
 
+  /**
+   * @brief Returns the best choice for a Layer object that can be used
+   * in RasterCache operations to cache the children of the OpacityLayer.
+   *
+   * The returned Layer must represent all children and try to remain stable
+   * if the OpacityLayer is reconstructed in subsequent frames of the scene.
+   *
+   * Note that since the synthetic child container returned from the
+   * GetChildContainer() method is created fresh with each new OpacityLayer,
+   * its return value will not be a good candidate for caching. But if the
+   * standard recommendations for animations are followed and the child widget
+   * is wrapped with a RepaintBoundary widget at the framework level, then
+   * the synthetic child container should contain the same single child layer
+   * on each frame. Under those conditions, that single child of the child
+   * container will be the best candidate for caching in the RasterCache
+   * and this method will return that single child if possible to improve
+   * the performance of caching the children.
+   *
+   * Note that if GetCacheableChild() does not find a single stable child of
+   * the child container it will return the child container as a fallback.
+   * Even though that child is new in each frame of an animation and thus we
+   * cannot reuse the cached layer raster between animation frames, the single
+   * container child will allow us to paint the child onto an offscreen buffer
+   * during Preroll() which reduces one render target switch compared to
+   * painting the child on the fly via an AutoSaveLayer in Paint() and thus
+   * still improves our performance.
+   *
+   * @see GetChildContainer()
+   * @return the best candidate Layer for caching the children
+   */
+  Layer* GetCacheableChild() const;
+
   SkAlpha alpha_;
   SkPoint offset_;
   SkRRect frameRRect_;
diff --git a/flow/layers/opacity_layer_unittests.cc b/flow/layers/opacity_layer_unittests.cc
index e08d5c9..735bb0a 100644
--- a/flow/layers/opacity_layer_unittests.cc
+++ b/flow/layers/opacity_layer_unittests.cc
@@ -53,6 +53,71 @@
 }
 #endif
 
+TEST_F(OpacityLayerTest, ChildIsCached) {
+  const SkAlpha alpha_half = 255 / 2;
+  auto initial_transform = SkMatrix::MakeTrans(50.0, 25.5);
+  auto other_transform = SkMatrix::MakeScale(1.0, 2.0);
+  const SkPath child_path = SkPath().addRect(SkRect::MakeWH(5.0f, 5.0f));
+  auto mock_layer = std::make_shared<MockLayer>(child_path);
+  auto layer =
+      std::make_shared<OpacityLayer>(alpha_half, SkPoint::Make(0.0f, 0.0f));
+  layer->Add(mock_layer);
+
+  SkMatrix cache_ctm = initial_transform;
+  SkCanvas cache_canvas;
+  cache_canvas.setMatrix(cache_ctm);
+  SkCanvas other_canvas;
+  other_canvas.setMatrix(other_transform);
+
+  use_mock_raster_cache();
+
+  EXPECT_EQ(raster_cache()->GetLayerCachedEntriesCount(), (size_t)0);
+  EXPECT_FALSE(raster_cache()->Draw(mock_layer.get(), other_canvas));
+  EXPECT_FALSE(raster_cache()->Draw(mock_layer.get(), cache_canvas));
+
+  layer->Preroll(preroll_context(), initial_transform);
+
+  EXPECT_EQ(raster_cache()->GetLayerCachedEntriesCount(), (size_t)1);
+  EXPECT_FALSE(raster_cache()->Draw(mock_layer.get(), other_canvas));
+  EXPECT_TRUE(raster_cache()->Draw(mock_layer.get(), cache_canvas));
+}
+
+TEST_F(OpacityLayerTest, ChildrenNotCached) {
+  const SkAlpha alpha_half = 255 / 2;
+  auto initial_transform = SkMatrix::MakeTrans(50.0, 25.5);
+  auto other_transform = SkMatrix::MakeScale(1.0, 2.0);
+  const SkPath child_path1 = SkPath().addRect(SkRect::MakeWH(5.0f, 5.0f));
+  const SkPath child_path2 = SkPath().addRect(SkRect::MakeWH(5.0f, 5.0f));
+  auto mock_layer1 = std::make_shared<MockLayer>(child_path1);
+  auto mock_layer2 = std::make_shared<MockLayer>(child_path2);
+  auto layer =
+      std::make_shared<OpacityLayer>(alpha_half, SkPoint::Make(0.0f, 0.0f));
+  layer->Add(mock_layer1);
+  layer->Add(mock_layer2);
+
+  SkMatrix cache_ctm = initial_transform;
+  SkCanvas cache_canvas;
+  cache_canvas.setMatrix(cache_ctm);
+  SkCanvas other_canvas;
+  other_canvas.setMatrix(other_transform);
+
+  use_mock_raster_cache();
+
+  EXPECT_EQ(raster_cache()->GetLayerCachedEntriesCount(), (size_t)0);
+  EXPECT_FALSE(raster_cache()->Draw(mock_layer1.get(), other_canvas));
+  EXPECT_FALSE(raster_cache()->Draw(mock_layer1.get(), cache_canvas));
+  EXPECT_FALSE(raster_cache()->Draw(mock_layer2.get(), other_canvas));
+  EXPECT_FALSE(raster_cache()->Draw(mock_layer2.get(), cache_canvas));
+
+  layer->Preroll(preroll_context(), initial_transform);
+
+  EXPECT_EQ(raster_cache()->GetLayerCachedEntriesCount(), (size_t)1);
+  EXPECT_FALSE(raster_cache()->Draw(mock_layer1.get(), other_canvas));
+  EXPECT_FALSE(raster_cache()->Draw(mock_layer1.get(), cache_canvas));
+  EXPECT_FALSE(raster_cache()->Draw(mock_layer2.get(), other_canvas));
+  EXPECT_FALSE(raster_cache()->Draw(mock_layer2.get(), cache_canvas));
+}
+
 TEST_F(OpacityLayerTest, FullyOpaque) {
   const SkPath child_path = SkPath().addRect(SkRect::MakeWH(5.0f, 5.0f));
   const SkPoint layer_offset = SkPoint::Make(0.5f, 1.5f);
diff --git a/flow/raster_cache.cc b/flow/raster_cache.cc
index d41408b..89fc1eb 100644
--- a/flow/raster_cache.cc
+++ b/flow/raster_cache.cc
@@ -86,7 +86,7 @@
 }
 
 /// @note Procedure doesn't copy all closures.
-static RasterCacheResult Rasterize(
+static std::unique_ptr<RasterCacheResult> Rasterize(
     GrContext* context,
     const SkMatrix& ctm,
     SkColorSpace* dst_color_space,
@@ -105,7 +105,7 @@
           : SkSurface::MakeRaster(image_info);
 
   if (!surface) {
-    return {};
+    return nullptr;
   }
 
   SkCanvas* canvas = surface->getCanvas();
@@ -118,14 +118,16 @@
     DrawCheckerboard(canvas, logical_rect);
   }
 
-  return {surface->makeImageSnapshot(), logical_rect};
+  return std::make_unique<RasterCacheResult>(surface->makeImageSnapshot(),
+                                             logical_rect);
 }
 
-RasterCacheResult RasterizePicture(SkPicture* picture,
-                                   GrContext* context,
-                                   const SkMatrix& ctm,
-                                   SkColorSpace* dst_color_space,
-                                   bool checkerboard) {
+std::unique_ptr<RasterCacheResult> RasterCache::RasterizePicture(
+    SkPicture* picture,
+    GrContext* context,
+    const SkMatrix& ctm,
+    SkColorSpace* dst_color_space,
+    bool checkerboard) const {
   return Rasterize(context, ctm, dst_color_space, checkerboard,
                    picture->cullRect(),
                    [=](SkCanvas* canvas) { canvas->drawPicture(picture); });
@@ -138,34 +140,41 @@
   Entry& entry = layer_cache_[cache_key];
   entry.access_count++;
   entry.used_this_frame = true;
-  if (!entry.image.is_valid()) {
-    entry.image = Rasterize(
-        context->gr_context, ctm, context->dst_color_space,
-        checkerboard_images_, layer->paint_bounds(),
-        [layer, context](SkCanvas* canvas) {
-          SkISize canvas_size = canvas->getBaseLayerSize();
-          SkNWayCanvas internal_nodes_canvas(canvas_size.width(),
-                                             canvas_size.height());
-          internal_nodes_canvas.addCanvas(canvas);
-          Layer::PaintContext paintContext = {
-              (SkCanvas*)&internal_nodes_canvas,
-              canvas,
-              context->gr_context,
-              nullptr,
-              context->raster_time,
-              context->ui_time,
-              context->texture_registry,
-              context->has_platform_view ? nullptr : context->raster_cache,
-              context->checkerboard_offscreen_layers,
-              context->frame_physical_depth,
-              context->frame_device_pixel_ratio};
-          if (layer->needs_painting()) {
-            layer->Paint(paintContext);
-          }
-        });
+  if (!entry.image) {
+    entry.image = RasterizeLayer(context, layer, ctm, checkerboard_images_);
   }
 }
 
+std::unique_ptr<RasterCacheResult> RasterCache::RasterizeLayer(
+    PrerollContext* context,
+    Layer* layer,
+    const SkMatrix& ctm,
+    bool checkerboard) const {
+  return Rasterize(
+      context->gr_context, ctm, context->dst_color_space, checkerboard,
+      layer->paint_bounds(), [layer, context](SkCanvas* canvas) {
+        SkISize canvas_size = canvas->getBaseLayerSize();
+        SkNWayCanvas internal_nodes_canvas(canvas_size.width(),
+                                           canvas_size.height());
+        internal_nodes_canvas.addCanvas(canvas);
+        Layer::PaintContext paintContext = {
+            (SkCanvas*)&internal_nodes_canvas,  // internal_nodes_canvas
+            canvas,                             // leaf_nodes_canvas
+            context->gr_context,                // gr_context
+            nullptr,                            // view_embedder
+            context->raster_time,
+            context->ui_time,
+            context->texture_registry,
+            context->has_platform_view ? nullptr : context->raster_cache,
+            context->checkerboard_offscreen_layers,
+            context->frame_physical_depth,
+            context->frame_device_pixel_ratio};
+        if (layer->needs_painting()) {
+          layer->Paint(paintContext);
+        }
+      });
+}
+
 bool RasterCache::Prepare(GrContext* context,
                           SkPicture* picture,
                           const SkMatrix& transformation_matrix,
@@ -202,7 +211,7 @@
     return false;
   }
 
-  if (!entry.image.is_valid()) {
+  if (!entry.image) {
     entry.image = RasterizePicture(picture, context, transformation_matrix,
                                    dst_color_space, checkerboard_images_);
     picture_cached_this_frame_++;
@@ -221,8 +230,8 @@
   entry.access_count++;
   entry.used_this_frame = true;
 
-  if (entry.image.is_valid()) {
-    entry.image.draw(canvas);
+  if (entry.image) {
+    entry.image->draw(canvas);
     return true;
   }
 
@@ -242,8 +251,8 @@
   entry.access_count++;
   entry.used_this_frame = true;
 
-  if (entry.image.is_valid()) {
-    entry.image.draw(canvas, paint);
+  if (entry.image) {
+    entry.image->draw(canvas, paint);
     return true;
   }
 
@@ -266,6 +275,14 @@
   return layer_cache_.size() + picture_cache_.size();
 }
 
+size_t RasterCache::GetLayerCachedEntriesCount() const {
+  return layer_cache_.size();
+}
+
+size_t RasterCache::GetPictureCachedEntriesCount() const {
+  return picture_cache_.size();
+}
+
 void RasterCache::SetCheckboardCacheImages(bool checkerboard) {
   if (checkerboard_images_ == checkerboard) {
     return;
@@ -287,15 +304,17 @@
   size_t picture_cache_bytes = 0;
 
   for (const auto& item : layer_cache_) {
-    const auto dimensions = item.second.image.image_dimensions();
     layer_cache_count++;
-    layer_cache_bytes += dimensions.width() * dimensions.height() * 4;
+    if (item.second.image) {
+      layer_cache_bytes += item.second.image->image_bytes();
+    }
   }
 
   for (const auto& item : picture_cache_) {
-    const auto dimensions = item.second.image.image_dimensions();
     picture_cache_count++;
-    picture_cache_bytes += dimensions.width() * dimensions.height() * 4;
+    if (item.second.image) {
+      picture_cache_bytes += item.second.image->image_bytes();
+    }
   }
 
   FML_TRACE_COUNTER("flutter", "RasterCache",
diff --git a/flow/raster_cache.h b/flow/raster_cache.h
index 3174ed0..a244846 100644
--- a/flow/raster_cache.h
+++ b/flow/raster_cache.h
@@ -19,22 +19,22 @@
 
 class RasterCacheResult {
  public:
-  RasterCacheResult() = default;
-
-  RasterCacheResult(const RasterCacheResult& other) = default;
-
   RasterCacheResult(sk_sp<SkImage> image, const SkRect& logical_rect);
 
-  operator bool() const { return static_cast<bool>(image_); }
+  virtual ~RasterCacheResult() = default;
 
-  bool is_valid() const { return static_cast<bool>(image_); };
+  virtual void draw(SkCanvas& canvas, const SkPaint* paint = nullptr) const;
 
-  void draw(SkCanvas& canvas, const SkPaint* paint = nullptr) const;
-
-  SkISize image_dimensions() const {
+  virtual SkISize image_dimensions() const {
     return image_ ? image_->dimensions() : SkISize::Make(0, 0);
   };
 
+  virtual int64_t image_bytes() const {
+    return image_ ? image_->dimensions().area() *
+                        image_->imageInfo().bytesPerPixel()
+                  : 0;
+  };
+
  private:
   sk_sp<SkImage> image_;
   SkRect logical_rect_;
@@ -54,6 +54,50 @@
       size_t access_threshold = 3,
       size_t picture_cache_limit_per_frame = kDefaultPictureCacheLimitPerFrame);
 
+  virtual ~RasterCache() = default;
+
+  /**
+   * @brief Rasterize a picture object and produce a RasterCacheResult
+   * to be stored in the cache.
+   *
+   * @param picture the SkPicture object to be cached.
+   * @param context the GrContext used for rendering.
+   * @param ctm the transformation matrix used for rendering.
+   * @param dst_color_space the destination color space that the cached
+   *        rendering will be drawn into
+   * @param checkerboard a flag indicating whether or not a checkerboard
+   *        pattern should be rendered into the cached image for debug
+   *        analysis
+   * @return a RasterCacheResult that can draw the rendered picture into
+   *         the destination using a simple image blit
+   */
+  virtual std::unique_ptr<RasterCacheResult> RasterizePicture(
+      SkPicture* picture,
+      GrContext* context,
+      const SkMatrix& ctm,
+      SkColorSpace* dst_color_space,
+      bool checkerboard) const;
+
+  /**
+   * @brief Rasterize an engine Layer and produce a RasterCacheResult
+   * to be stored in the cache.
+   *
+   * @param context the PrerollContext containing important information
+   *        needed for rendering a layer.
+   * @param layer the Layer object to be cached.
+   * @param ctm the transformation matrix used for rendering.
+   * @param checkerboard a flag indicating whether or not a checkerboard
+   *        pattern should be rendered into the cached image for debug
+   *        analysis
+   * @return a RasterCacheResult that can draw the rendered layer into
+   *         the destination using a simple image blit
+   */
+  virtual std::unique_ptr<RasterCacheResult> RasterizeLayer(
+      PrerollContext* context,
+      Layer* layer,
+      const SkMatrix& ctm,
+      bool checkerboard) const;
+
   static SkIRect GetDeviceBounds(const SkRect& rect, const SkMatrix& ctm) {
     SkRect device_rect;
     ctm.mapRect(&device_rect, rect);
@@ -123,11 +167,15 @@
 
   size_t GetCachedEntriesCount() const;
 
+  size_t GetLayerCachedEntriesCount() const;
+
+  size_t GetPictureCachedEntriesCount() const;
+
  private:
   struct Entry {
     bool used_this_frame = false;
     size_t access_count = 0;
-    RasterCacheResult image;
+    std::unique_ptr<RasterCacheResult> image;
   };
 
   template <class Cache>
diff --git a/flow/testing/layer_test.h b/flow/testing/layer_test.h
index 593dec1..a81bf3c 100644
--- a/flow/testing/layer_test.h
+++ b/flow/testing/layer_test.h
@@ -13,6 +13,7 @@
 #include "flutter/fml/macros.h"
 #include "flutter/testing/canvas_test.h"
 #include "flutter/testing/mock_canvas.h"
+#include "flutter/testing/mock_raster_cache.h"
 #include "third_party/skia/include/core/SkCanvas.h"
 #include "third_party/skia/include/core/SkImageInfo.h"
 #include "third_party/skia/include/utils/SkNWayCanvas.h"
@@ -24,6 +25,13 @@
 // |Layer|'s.
 // |LayerTest| is a default implementation based on |::testing::Test|.
 //
+// By default the preroll and paint contexts will not use a raster cache.
+// If a test needs to verify the proper operation of a layer in the presence
+// of a raster cache then a number of options can be enabled by using the
+// methods |LayerTestBase::use_null_raster_cache()|,
+// |LayerTestBase::use_mock_raster_cache()| or
+// |LayerTestBase::use_skia_raster_cache()|
+//
 // |BaseT| should be the base test type, such as |::testing::Test| below.
 template <typename BaseT>
 class LayerTestBase : public CanvasTestBase<BaseT> {
@@ -55,18 +63,77 @@
             false,   /* checkerboard_offscreen_layers */
             100.0f,  /* frame_physical_depth */
             1.0f,    /* frame_device_pixel_ratio */
-        }) {}
+        }) {
+    use_null_raster_cache();
+  }
+
+  /**
+   * @brief Use no raster cache in the preroll_context() and
+   * paint_context() structures.
+   *
+   * This method must be called before using the preroll_context() and
+   * paint_context() structures in calls to the Layer::Preroll() and
+   * Layer::Paint() methods. This is the default mode of operation.
+   *
+   * @see use_mock_raster_cache()
+   * @see use_skia_raster_cache()
+   */
+  void use_null_raster_cache() { set_raster_cache_(nullptr); }
+
+  /**
+   * @brief Use a mock raster cache in the preroll_context() and
+   * paint_context() structures.
+   *
+   * This method must be called before using the preroll_context() and
+   * paint_context() structures in calls to the Layer::Preroll() and
+   * Layer::Paint() methods. The mock raster cache behaves like a normal
+   * raster cache with respect to decisions about when layers and pictures
+   * should be cached, but it does not incur the overhead of rendering the
+   * layers or caching the resulting pixels.
+   *
+   * @see use_null_raster_cache()
+   * @see use_skia_raster_cache()
+   */
+  void use_mock_raster_cache() {
+    set_raster_cache_(std::make_unique<MockRasterCache>());
+  }
+
+  /**
+   * @brief Use a normal raster cache in the preroll_context() and
+   * paint_context() structures.
+   *
+   * This method must be called before using the preroll_context() and
+   * paint_context() structures in calls to the Layer::Preroll() and
+   * Layer::Paint() methods. The Skia raster cache will behave identically
+   * to the raster cache typically used when handling a frame on a device
+   * including rendering the contents of pictures and layers to an
+   * SkImage, but using a software rather than a hardware renderer.
+   *
+   * @see use_null_raster_cache()
+   * @see use_mock_raster_cache()
+   */
+  void use_skia_raster_cache() {
+    set_raster_cache_(std::make_unique<RasterCache>());
+  }
 
   TextureRegistry& texture_regitry() { return texture_registry_; }
+  RasterCache* raster_cache() { return raster_cache_.get(); }
   PrerollContext* preroll_context() { return &preroll_context_; }
   Layer::PaintContext& paint_context() { return paint_context_; }
 
  private:
+  void set_raster_cache_(std::unique_ptr<RasterCache> raster_cache) {
+    raster_cache_ = std::move(raster_cache);
+    preroll_context_.raster_cache = raster_cache_.get();
+    paint_context_.raster_cache = raster_cache_.get();
+  }
+
   Stopwatch raster_time_;
   Stopwatch ui_time_;
   MutatorsStack mutators_stack_;
   TextureRegistry texture_registry_;
 
+  std::unique_ptr<RasterCache> raster_cache_;
   PrerollContext preroll_context_;
   Layer::PaintContext paint_context_;
 
diff --git a/testing/BUILD.gn b/testing/BUILD.gn
index b8c3bd0..69f7d32 100644
--- a/testing/BUILD.gn
+++ b/testing/BUILD.gn
@@ -71,10 +71,13 @@
     "canvas_test.h",
     "mock_canvas.cc",
     "mock_canvas.h",
+    "mock_raster_cache.cc",
+    "mock_raster_cache.h",
   ]
 
   public_deps = [
     ":testing_lib",
+    "//flutter/flow",
     "//third_party/skia",
   ]
 }
diff --git a/testing/mock_raster_cache.cc b/testing/mock_raster_cache.cc
new file mode 100644
index 0000000..7b178ff
--- /dev/null
+++ b/testing/mock_raster_cache.cc
@@ -0,0 +1,39 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "flutter/testing/mock_raster_cache.h"
+#include "flutter/flow/layers/layer.h"
+
+namespace flutter {
+namespace testing {
+
+MockRasterCacheResult::MockRasterCacheResult(SkIRect device_rect)
+    : RasterCacheResult(nullptr, SkRect::MakeEmpty()),
+      device_rect_(device_rect) {}
+
+std::unique_ptr<RasterCacheResult> MockRasterCache::RasterizePicture(
+    SkPicture* picture,
+    GrContext* context,
+    const SkMatrix& ctm,
+    SkColorSpace* dst_color_space,
+    bool checkerboard) const {
+  SkRect logical_rect = picture->cullRect();
+  SkIRect cache_rect = RasterCache::GetDeviceBounds(logical_rect, ctm);
+
+  return std::make_unique<MockRasterCacheResult>(cache_rect);
+}
+
+std::unique_ptr<RasterCacheResult> MockRasterCache::RasterizeLayer(
+    PrerollContext* context,
+    Layer* layer,
+    const SkMatrix& ctm,
+    bool checkerboard) const {
+  SkRect logical_rect = layer->paint_bounds();
+  SkIRect cache_rect = RasterCache::GetDeviceBounds(logical_rect, ctm);
+
+  return std::make_unique<MockRasterCacheResult>(cache_rect);
+}
+
+}  // namespace testing
+}  // namespace flutter
diff --git a/testing/mock_raster_cache.h b/testing/mock_raster_cache.h
new file mode 100644
index 0000000..0f85118
--- /dev/null
+++ b/testing/mock_raster_cache.h
@@ -0,0 +1,64 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef TESTING_MOCK_RASTER_CACHE_H_
+#define TESTING_MOCK_RASTER_CACHE_H_
+
+#include "flutter/flow/raster_cache.h"
+#include "third_party/skia/include/core/SkImage.h"
+#include "third_party/skia/include/core/SkPicture.h"
+
+namespace flutter {
+namespace testing {
+
+/**
+ * @brief A RasterCacheResult implementation that represents a cached Layer or
+ * SkPicture without the overhead of storage.
+ *
+ * This implementation is used by MockRasterCache only for testing proper usage
+ * of the RasterCache in layer unit tests.
+ */
+class MockRasterCacheResult : public RasterCacheResult {
+ public:
+  MockRasterCacheResult(SkIRect device_rect);
+
+  void draw(SkCanvas& canvas, const SkPaint* paint = nullptr) const override{};
+
+  SkISize image_dimensions() const override { return device_rect_.size(); };
+
+  int64_t image_bytes() const override {
+    return image_dimensions().area() *
+           SkColorTypeBytesPerPixel(kBGRA_8888_SkColorType);
+  }
+
+ private:
+  SkIRect device_rect_;
+};
+
+/**
+ * @brief A RasterCache implementation that simulates the act of rendering a
+ * Layer or SkPicture without the overhead of rasterization or pixel storage.
+ * This implementation is used only for testing proper usage of the RasterCache
+ * in layer unit tests.
+ */
+class MockRasterCache : public RasterCache {
+ public:
+  std::unique_ptr<RasterCacheResult> RasterizePicture(
+      SkPicture* picture,
+      GrContext* context,
+      const SkMatrix& ctm,
+      SkColorSpace* dst_color_space,
+      bool checkerboard) const override;
+
+  std::unique_ptr<RasterCacheResult> RasterizeLayer(
+      PrerollContext* context,
+      Layer* layer,
+      const SkMatrix& ctm,
+      bool checkerboard) const override;
+};
+
+}  // namespace testing
+}  // namespace flutter
+
+#endif  // TESTING_MOCK_RASTER_CACHE_H_