Bug 1711061 - Part 9. Add blob recording support to SurfaceCache. r=tnikkel
☠☠ backed out by a129e5ede22c ☠ ☠
authorAndrew Osmond <aosmond@mozilla.com>
Tue, 26 Oct 2021 13:28:26 +0000
changeset 596948 27565d5ae08de12e6af08077232229d85b26752e
parent 596947 f3b2379d971be27ff01455f5a8fe66a5846c4e6e
child 596949 000940244dcf4ed6dd3ecf6d5ca07f367a59b56b
push id38915
push usermlaza@mozilla.com
push dateTue, 26 Oct 2021 21:44:17 +0000
treeherdermozilla-central@7708adfc84d3 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerstnikkel
bugs1711061
milestone95.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 1711061 - Part 9. Add blob recording support to SurfaceCache. r=tnikkel Now that we no longer have the extra layer of ImageContainers providing a superficial level of caching/reuse of existing blob recordings, we need some way to share recordings. This part adds support to SurfaceCache to store BlobSurfaceProvider objects. This includes the specialized code for invalidating SVG images. In particular this is useful for animated SVG images. In general we want to avoid changing the image key whenever possible so that we avoid reallocating the underlying buffers in the compositor process for the rasterized blob images. We also need to track the ImageIntRegion used by the recording. If a caller only wants a slice of the SVG image, then we need to track this differentiation in our cache entries. At this time, we don't allow substitutes for entries with a region exclusion. Differential Revision: https://phabricator.services.mozilla.com/D126603
image/ImageRegion.h
image/SurfaceCache.cpp
image/SurfaceCache.h
image/SurfaceFlags.h
image/imgLoader.cpp
--- a/image/ImageRegion.h
+++ b/image/ImageRegion.h
@@ -8,16 +8,17 @@
 
 #include "gfxMatrix.h"
 #include "gfxPoint.h"
 #include "gfxRect.h"
 #include "gfxTypes.h"
 #include "mozilla/gfx/Matrix.h"
 #include "mozilla/gfx/Types.h"
 #include "nsSize.h"
+#include "PLDHashTable.h"  // for PLDHashNumber
 
 namespace mozilla {
 namespace image {
 
 /**
  * An axis-aligned rectangle in tiled image space, with an optional sampling
  * restriction rect. The drawing code ensures that if a sampling restriction
  * rect is present, any pixels sampled during the drawing process are found
@@ -234,16 +235,22 @@ class ImageIntRegion {
 
   bool operator==(const ImageIntRegion& aOther) const {
     return mExtendMode == aOther.mExtendMode &&
            mIsRestricted == aOther.mIsRestricted &&
            mRect.IsEqualEdges(aOther.mRect) &&
            (!mIsRestricted || mRestriction.IsEqualEdges(aOther.mRestriction));
   }
 
+  PLDHashNumber Hash() const {
+    return HashGeneric(mRect.x, mRect.y, mRect.width, mRect.height,
+                       mRestriction.x, mRestriction.y, mRestriction.width,
+                       mRestriction.height, mExtendMode, mIsRestricted);
+  }
+
   /* ImageIntRegion() : mIsRestricted(false) { } */
 
  private:
   explicit ImageIntRegion(const mozilla::gfx::IntRect& aRect,
                           ExtendMode aExtendMode)
       : mRect(aRect), mExtendMode(aExtendMode), mIsRestricted(false) {}
 
   ImageIntRegion(const mozilla::gfx::IntRect& aRect,
--- a/image/SurfaceCache.cpp
+++ b/image/SurfaceCache.cpp
@@ -171,16 +171,18 @@ class CachedSurface {
   CostEntry GetCostEntry() {
     return image::CostEntry(WrapNotNull(this), mProvider->LogicalSizeInBytes());
   }
 
   size_t ShallowSizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const {
     return aMallocSizeOf(this) + aMallocSizeOf(mProvider.get());
   }
 
+  void InvalidateRecording() { mProvider->InvalidateRecording(); }
+
   // A helper type used by SurfaceCacheImpl::CollectSizeOfSurfaces.
   struct MOZ_STACK_CLASS SurfaceMemoryReport {
     SurfaceMemoryReport(nsTArray<SurfaceMemoryCounter>& aCounters,
                         MallocSizeOf aMallocSizeOf)
         : mCounters(aCounters), mMallocSizeOf(aMallocSizeOf) {}
 
     void Add(NotNull<CachedSurface*> aCachedSurface, bool aIsFactor2) {
       if (aCachedSurface->IsPlaceholder()) {
@@ -271,18 +273,24 @@ class ImageSurfaceCache {
       bytes += value->ShallowSizeOfIncludingThis(aMallocSizeOf);
     }
     return bytes;
   }
 
   [[nodiscard]] bool Insert(NotNull<CachedSurface*> aSurface) {
     MOZ_ASSERT(!mLocked || aSurface->IsPlaceholder() || aSurface->IsLocked(),
                "Inserting an unlocked surface for a locked image");
-    return mSurfaces.InsertOrUpdate(aSurface->GetSurfaceKey(),
-                                    RefPtr<CachedSurface>{aSurface}, fallible);
+    const auto& surfaceKey = aSurface->GetSurfaceKey();
+    if (surfaceKey.Region()) {
+      // We don't allow substitutes for surfaces with regions, so we don't want
+      // to allow factor of 2 mode pruning to release these surfaces.
+      aSurface->SetCannotSubstitute();
+    }
+    return mSurfaces.InsertOrUpdate(surfaceKey, RefPtr<CachedSurface>{aSurface},
+                                    fallible);
   }
 
   already_AddRefed<CachedSurface> Remove(NotNull<CachedSurface*> aSurface) {
     MOZ_ASSERT(mSurfaces.GetWeak(aSurface->GetSurfaceKey()),
                "Should not be removing a surface we don't have");
 
     RefPtr<CachedSurface> surface;
     mSurfaces.Remove(aSurface->GetSurfaceKey(), getter_AddRefs(surface));
@@ -321,16 +329,20 @@ class ImageSurfaceCache {
       const SurfaceKey& aIdealKey) {
     // Try for an exact match first.
     RefPtr<CachedSurface> exactMatch;
     mSurfaces.Get(aIdealKey, getter_AddRefs(exactMatch));
     if (exactMatch) {
       if (exactMatch->IsDecoded()) {
         return MakeTuple(exactMatch.forget(), MatchType::EXACT, IntSize());
       }
+    } else if (aIdealKey.Region()) {
+      // We cannot substitute if we have a region. Allow it to create an exact
+      // match.
+      return MakeTuple(exactMatch.forget(), MatchType::NOT_FOUND, IntSize());
     } else if (!mFactor2Mode) {
       // If no exact match is found, and we are not in factor of 2 mode, then
       // we know that we will trigger a decode because at best we will provide
       // a substitute. Make sure we switch now to factor of 2 mode if necessary.
       MaybeSetFactor2Mode();
     }
 
     // Try for a best match second, if using compact.
@@ -348,18 +360,18 @@ class ImageSurfaceCache {
     }
 
     // There's no perfect match, so find the best match we can.
     RefPtr<CachedSurface> bestMatch;
     for (const auto& value : Values()) {
       NotNull<CachedSurface*> current = WrapNotNull(value);
       const SurfaceKey& currentKey = current->GetSurfaceKey();
 
-      // We never match a placeholder.
-      if (current->IsPlaceholder()) {
+      // We never match a placeholder or a surface with a region.
+      if (current->IsPlaceholder() || currentKey.Region()) {
         continue;
       }
       // Matching the playback type and SVG context is required.
       if (currentKey.Playback() != aIdealKey.Playback() ||
           currentKey.SVGContext() != aIdealKey.SVGContext()) {
         continue;
       }
       // Matching the flags is required.
@@ -525,16 +537,38 @@ class ImageSurfaceCache {
     }
 
     // We should never leave factor of 2 mode due to pruning in of itself, but
     // if we discarded surfaces due to the volatile buffers getting released,
     // it is possible.
     AfterMaybeRemove();
   }
 
+  template <typename Function>
+  bool Invalidate(Function&& aRemoveCallback) {
+    // Remove all non-blob recordings from the cache. Invalidate any blob
+    // recordings.
+    bool foundRecording = false;
+    for (auto iter = mSurfaces.Iter(); !iter.Done(); iter.Next()) {
+      NotNull<CachedSurface*> current = WrapNotNull(iter.UserData());
+
+      if (current->GetSurfaceKey().Flags() & SurfaceFlags::RECORD_BLOB) {
+        foundRecording = true;
+        current->InvalidateRecording();
+        continue;
+      }
+
+      aRemoveCallback(current);
+      iter.Remove();
+    }
+
+    AfterMaybeRemove();
+    return foundRecording;
+  }
+
   IntSize SuggestedSize(const IntSize& aSize) const {
     IntSize suggestedSize = SuggestedSizeInternal(aSize);
     if (mIsVectorImage) {
       suggestedSize = SurfaceCache::ClampVectorSize(suggestedSize);
     }
     return suggestedSize;
   }
 
@@ -1011,17 +1045,18 @@ class SurfaceCacheImpl final : public ns
       Remove(WrapNotNull(surface), /* aStopTracking */ true, aAutoLock);
     }
 
     MOZ_ASSERT_IF(matchType == MatchType::EXACT,
                   surface->GetSurfaceKey() == aSurfaceKey);
     MOZ_ASSERT_IF(
         matchType == MatchType::SUBSTITUTE_BECAUSE_NOT_FOUND ||
             matchType == MatchType::SUBSTITUTE_BECAUSE_PENDING,
-        surface->GetSurfaceKey().SVGContext() == aSurfaceKey.SVGContext() &&
+        surface->GetSurfaceKey().Region() == aSurfaceKey.Region() &&
+            surface->GetSurfaceKey().SVGContext() == aSurfaceKey.SVGContext() &&
             surface->GetSurfaceKey().Playback() == aSurfaceKey.Playback() &&
             surface->GetSurfaceKey().Flags() == aSurfaceKey.Flags());
 
     if (matchType == MatchType::EXACT ||
         matchType == MatchType::SUBSTITUTE_BECAUSE_BEST) {
       if (aMarkUsed &&
           !MarkUsed(WrapNotNull(surface), WrapNotNull(cache), aAutoLock)) {
         Remove(WrapNotNull(surface), /* aStopTracking */ false, aAutoLock);
@@ -1127,16 +1162,34 @@ class SurfaceCacheImpl final : public ns
       StopTracking(aSurface, /* aIsTracked */ true, aAutoLock);
       // Individual surfaces must be freed outside the lock.
       mCachedSurfacesDiscard.AppendElement(aSurface);
     });
 
     MaybeRemoveEmptyCache(aImageKey, cache);
   }
 
+  bool InvalidateImage(const ImageKey aImageKey,
+                       const StaticMutexAutoLock& aAutoLock) {
+    RefPtr<ImageSurfaceCache> cache = GetImageCache(aImageKey);
+    if (!cache) {
+      return false;  // No cached surfaces for this image, so nothing to do.
+    }
+
+    bool rv = cache->Invalidate(
+        [this, &aAutoLock](NotNull<CachedSurface*> aSurface) -> void {
+          StopTracking(aSurface, /* aIsTracked */ true, aAutoLock);
+          // Individual surfaces must be freed outside the lock.
+          mCachedSurfacesDiscard.AppendElement(aSurface);
+        });
+
+    MaybeRemoveEmptyCache(aImageKey, cache);
+    return rv;
+  }
+
   void DiscardAll(const StaticMutexAutoLock& aAutoLock) {
     // Remove in order of cost because mCosts is an array and the other data
     // structures are all hash tables. Note that locked surfaces are not
     // removed, since they aren't present in mCosts.
     while (!mCosts.IsEmpty()) {
       Remove(mCosts.LastElement().Surface(), /* aStopTracking */ true,
              aAutoLock);
     }
@@ -1713,16 +1766,30 @@ void SurfaceCache::PruneImage(const Imag
     if (sInstance) {
       sInstance->PruneImage(aImageKey, lock);
       sInstance->TakeDiscard(discard, lock);
     }
   }
 }
 
 /* static */
+bool SurfaceCache::InvalidateImage(const ImageKey aImageKey) {
+  nsTArray<RefPtr<CachedSurface>> discard;
+  bool rv = false;
+  {
+    StaticMutexAutoLock lock(sInstanceMutex);
+    if (sInstance) {
+      rv = sInstance->InvalidateImage(aImageKey, lock);
+      sInstance->TakeDiscard(discard, lock);
+    }
+  }
+  return rv;
+}
+
+/* static */
 void SurfaceCache::DiscardAll() {
   nsTArray<RefPtr<CachedSurface>> discard;
   {
     StaticMutexAutoLock lock(sInstanceMutex);
     if (sInstance) {
       sInstance->DiscardAll(lock);
       sInstance->TakeDiscard(discard, lock);
     }
--- a/image/SurfaceCache.h
+++ b/image/SurfaceCache.h
@@ -16,16 +16,17 @@
 #include "mozilla/MemoryReporting.h"  // for MallocSizeOf
 #include "mozilla/NotNull.h"
 #include "mozilla/SVGImageContext.h"  // for SVGImageContext
 #include "mozilla/gfx/2D.h"           // for SourceSurface
 #include "mozilla/gfx/Point.h"        // for mozilla::gfx::IntSize
 #include "gfx2DGlue.h"
 #include "gfxPoint.h"  // for gfxSize
 #include "nsCOMPtr.h"  // for already_AddRefed
+#include "ImageRegion.h"
 #include "PlaybackType.h"
 #include "SurfaceFlags.h"
 
 namespace mozilla {
 namespace image {
 
 class ImageResource;
 class ISurfaceProvider;
@@ -47,83 +48,106 @@ using ImageKey = ImageResource*;
  * Callers should construct a SurfaceKey using the appropriate helper function
  * for their image type - either RasterSurfaceKey or VectorSurfaceKey.
  */
 class SurfaceKey {
   typedef gfx::IntSize IntSize;
 
  public:
   bool operator==(const SurfaceKey& aOther) const {
-    return aOther.mSize == mSize && aOther.mSVGContext == mSVGContext &&
-           aOther.mPlayback == mPlayback && aOther.mFlags == mFlags;
+    return aOther.mSize == mSize && aOther.mRegion == mRegion &&
+           aOther.mSVGContext == mSVGContext && aOther.mPlayback == mPlayback &&
+           aOther.mFlags == mFlags;
   }
 
   PLDHashNumber Hash() const {
     PLDHashNumber hash = HashGeneric(mSize.width, mSize.height);
+    hash = AddToHash(hash, mRegion.map(HashIIR).valueOr(0));
     hash = AddToHash(hash, mSVGContext.map(HashSIC).valueOr(0));
     hash = AddToHash(hash, uint8_t(mPlayback), uint32_t(mFlags));
     return hash;
   }
 
   SurfaceKey CloneWithSize(const IntSize& aSize) const {
-    return SurfaceKey(aSize, mSVGContext, mPlayback, mFlags);
+    return SurfaceKey(aSize, mRegion, mSVGContext, mPlayback, mFlags);
   }
 
   const IntSize& Size() const { return mSize; }
+  const Maybe<ImageIntRegion>& Region() const { return mRegion; }
   const Maybe<SVGImageContext>& SVGContext() const { return mSVGContext; }
   PlaybackType Playback() const { return mPlayback; }
   SurfaceFlags Flags() const { return mFlags; }
 
  private:
-  SurfaceKey(const IntSize& aSize, const Maybe<SVGImageContext>& aSVGContext,
-             PlaybackType aPlayback, SurfaceFlags aFlags)
+  SurfaceKey(const IntSize& aSize, const Maybe<ImageIntRegion>& aRegion,
+             const Maybe<SVGImageContext>& aSVGContext, PlaybackType aPlayback,
+             SurfaceFlags aFlags)
       : mSize(aSize),
+        mRegion(aRegion),
         mSVGContext(aSVGContext),
         mPlayback(aPlayback),
         mFlags(aFlags) {}
 
+  static PLDHashNumber HashIIR(const ImageIntRegion& aIIR) {
+    return aIIR.Hash();
+  }
+
   static PLDHashNumber HashSIC(const SVGImageContext& aSIC) {
     return aSIC.Hash();
   }
 
   friend SurfaceKey RasterSurfaceKey(const IntSize&, SurfaceFlags,
                                      PlaybackType);
   friend SurfaceKey VectorSurfaceKey(const IntSize&,
                                      const Maybe<SVGImageContext>&);
+  friend SurfaceKey VectorSurfaceKey(const IntSize&,
+                                     const Maybe<ImageIntRegion>&,
+                                     const Maybe<SVGImageContext>&,
+                                     SurfaceFlags, PlaybackType);
   friend SurfaceKey ContainerSurfaceKey(
       const gfx::IntSize& aSize, const Maybe<SVGImageContext>& aSVGContext,
       SurfaceFlags aFlags);
 
   IntSize mSize;
+  Maybe<ImageIntRegion> mRegion;
   Maybe<SVGImageContext> mSVGContext;
   PlaybackType mPlayback;
   SurfaceFlags mFlags;
 };
 
 inline SurfaceKey RasterSurfaceKey(const gfx::IntSize& aSize,
                                    SurfaceFlags aFlags,
                                    PlaybackType aPlayback) {
-  return SurfaceKey(aSize, Nothing(), aPlayback, aFlags);
+  return SurfaceKey(aSize, Nothing(), Nothing(), aPlayback, aFlags);
+}
+
+inline SurfaceKey VectorSurfaceKey(const gfx::IntSize& aSize,
+                                   const Maybe<ImageIntRegion>& aRegion,
+                                   const Maybe<SVGImageContext>& aSVGContext,
+                                   SurfaceFlags aFlags,
+                                   PlaybackType aPlayback) {
+  return SurfaceKey(aSize, aRegion, aSVGContext, aPlayback, aFlags);
 }
 
 inline SurfaceKey VectorSurfaceKey(const gfx::IntSize& aSize,
                                    const Maybe<SVGImageContext>& aSVGContext) {
   // We don't care about aFlags for VectorImage because none of the flags we
   // have right now influence VectorImage's rendering. If we add a new flag that
   // *does* affect how a VectorImage renders, we'll have to change this.
   // Similarly, we don't accept a PlaybackType parameter because we don't
   // currently cache frames of animated SVG images.
-  return SurfaceKey(aSize, aSVGContext, PlaybackType::eStatic,
+  return SurfaceKey(aSize, Nothing(), aSVGContext, PlaybackType::eStatic,
                     DefaultSurfaceFlags());
 }
 
 inline SurfaceKey ContainerSurfaceKey(const gfx::IntSize& aSize,
                                       const Maybe<SVGImageContext>& aSVGContext,
                                       SurfaceFlags aFlags) {
-  return SurfaceKey(aSize, aSVGContext, PlaybackType::eStatic, aFlags);
+  return SurfaceKey(aSize, Nothing(), aSVGContext, PlaybackType::eStatic,
+                    aFlags);
 }
 
 /**
  * AvailabilityState is used to track whether an ISurfaceProvider has a surface
  * available or is just a placeholder.
  *
  * To ensure that availability changes are atomic (and especially that internal
  * SurfaceCache code doesn't have to deal with asynchronous availability
@@ -405,16 +429,27 @@ struct SurfaceCache {
    * it is able substitute that entry with. Note that this only applies if the
    * image is in factor of 2 mode. If it is not, this operation does nothing.
    *
    * @param aImageKey  The image whose cache which should be pruned.
    */
   static void PruneImage(const ImageKey aImageKey);
 
   /**
+   * Removes all rasterized cache entries (including placeholders) associated
+   * with the given image from the cache. Any blob recordings are marked as
+   * dirty and must be regenerated.
+   *
+   * @param aImageKey  The image whose cache which should be regenerated.
+   *
+   * @returns true if any recordings were invalidated, else false.
+   */
+  static bool InvalidateImage(const ImageKey aImageKey);
+
+  /**
    * Evicts all evictable entries from the cache.
    *
    * All entries are evictable except for entries associated with locked images.
    * Non-evictable entries can only be removed by RemoveImage().
    */
   static void DiscardAll();
 
   /**
--- a/image/SurfaceFlags.h
+++ b/image/SurfaceFlags.h
@@ -15,17 +15,18 @@ namespace image {
 /**
  * Flags that change the output a decoder generates. Because different
  * combinations of these flags result in logically different surfaces, these
  * flags must be taken into account in SurfaceCache lookups.
  */
 enum class SurfaceFlags : uint8_t {
   NO_PREMULTIPLY_ALPHA = 1 << 0,
   NO_COLORSPACE_CONVERSION = 1 << 1,
-  TO_SRGB_COLORSPACE = 2 << 1,
+  TO_SRGB_COLORSPACE = 1 << 2,
+  RECORD_BLOB = 1 << 3,
 };
 MOZ_MAKE_ENUM_CLASS_BITWISE_OPERATORS(SurfaceFlags)
 
 /**
  * @return the default set of surface flags.
  */
 inline SurfaceFlags DefaultSurfaceFlags() { return SurfaceFlags(); }
 
@@ -39,16 +40,19 @@ inline SurfaceFlags ToSurfaceFlags(uint3
     flags |= SurfaceFlags::NO_PREMULTIPLY_ALPHA;
   }
   if (aFlags & imgIContainer::FLAG_DECODE_NO_COLORSPACE_CONVERSION) {
     flags |= SurfaceFlags::NO_COLORSPACE_CONVERSION;
   }
   if (aFlags & imgIContainer::FLAG_DECODE_TO_SRGB_COLORSPACE) {
     flags |= SurfaceFlags::TO_SRGB_COLORSPACE;
   }
+  if (aFlags & imgIContainer::FLAG_RECORD_BLOB) {
+    flags |= SurfaceFlags::RECORD_BLOB;
+  }
   return flags;
 }
 
 /**
  * Given a set of SurfaceFlags, returns a set of imgIContainer FLAG_* flags with
  * the corresponding flags set.
  */
 inline uint32_t FromSurfaceFlags(SurfaceFlags aFlags) {
@@ -57,15 +61,18 @@ inline uint32_t FromSurfaceFlags(Surface
     flags |= imgIContainer::FLAG_DECODE_NO_PREMULTIPLY_ALPHA;
   }
   if (aFlags & SurfaceFlags::NO_COLORSPACE_CONVERSION) {
     flags |= imgIContainer::FLAG_DECODE_NO_COLORSPACE_CONVERSION;
   }
   if (aFlags & SurfaceFlags::TO_SRGB_COLORSPACE) {
     flags |= imgIContainer::FLAG_DECODE_TO_SRGB_COLORSPACE;
   }
+  if (aFlags & SurfaceFlags::RECORD_BLOB) {
+    flags |= imgIContainer::FLAG_RECORD_BLOB;
+  }
   return flags;
 }
 
 }  // namespace image
 }  // namespace mozilla
 
 #endif  // mozilla_image_SurfaceFlags_h
--- a/image/imgLoader.cpp
+++ b/image/imgLoader.cpp
@@ -394,16 +394,49 @@ class imgMemoryReporter final : public n
       }
 
       if (counter.Key().Flags() != DefaultSurfaceFlags()) {
         surfacePathPrefix.AppendLiteral(", flags:");
         surfacePathPrefix.AppendInt(uint32_t(counter.Key().Flags()),
                                     /* aRadix = */ 16);
       }
 
+      if (counter.Key().Region()) {
+        const ImageIntRegion& region = counter.Key().Region().ref();
+        const gfx::IntRect& rect = region.Rect();
+        surfacePathPrefix.AppendLiteral(", region:[ rect=(");
+        surfacePathPrefix.AppendInt(rect.x);
+        surfacePathPrefix.AppendLiteral(",");
+        surfacePathPrefix.AppendInt(rect.y);
+        surfacePathPrefix.AppendLiteral(") ");
+        surfacePathPrefix.AppendInt(rect.width);
+        surfacePathPrefix.AppendLiteral("x");
+        surfacePathPrefix.AppendInt(rect.height);
+        if (region.IsRestricted()) {
+          const gfx::IntRect& restrict = region.Restriction();
+          if (restrict == rect) {
+            surfacePathPrefix.AppendLiteral(", restrict=rect");
+          } else {
+            surfacePathPrefix.AppendLiteral(", restrict=(");
+            surfacePathPrefix.AppendInt(restrict.x);
+            surfacePathPrefix.AppendLiteral(",");
+            surfacePathPrefix.AppendInt(restrict.y);
+            surfacePathPrefix.AppendLiteral(") ");
+            surfacePathPrefix.AppendInt(restrict.width);
+            surfacePathPrefix.AppendLiteral("x");
+            surfacePathPrefix.AppendInt(restrict.height);
+          }
+        }
+        if (region.GetExtendMode() != gfx::ExtendMode::CLAMP) {
+          surfacePathPrefix.AppendLiteral(", extendMode=");
+          surfacePathPrefix.AppendInt(int32_t(region.GetExtendMode()));
+        }
+        surfacePathPrefix.AppendLiteral("]");
+      }
+
       if (counter.Key().SVGContext()) {
         const SVGImageContext& context = counter.Key().SVGContext().ref();
         surfacePathPrefix.AppendLiteral(", svgContext:[ ");
         if (context.GetViewportSize()) {
           const CSSIntSize& size = context.GetViewportSize().ref();
           surfacePathPrefix.AppendLiteral("viewport=(");
           surfacePathPrefix.AppendInt(size.width);
           surfacePathPrefix.AppendLiteral("x");