Bug 1370412 - Part 8b. Add ImageSurfaceCache::Prune to discard surfaces which are not needed in factor of 2 mode. r=tnikkel
authorAndrew Osmond <aosmond@mozilla.com>
Tue, 05 Sep 2017 07:58:46 -0400
changeset 659180 85cdb40c26211ea3288c46927641e14f0f5661d9
parent 659179 c856f513264340a2d6c2b70f71f345b5e4c8d6c0
child 659181 5cd1c5cd9b0154c25a5bb8971dfb5e7fc0583987
push id78047
push userbmo:francesco.lodolo@gmail.com
push dateTue, 05 Sep 2017 17:25:17 +0000
reviewerstnikkel
bugs1370412
milestone57.0a1
Bug 1370412 - Part 8b. Add ImageSurfaceCache::Prune to discard surfaces which are not needed in factor of 2 mode. r=tnikkel
image/DecodedSurfaceProvider.cpp
image/SurfaceCache.cpp
image/SurfaceCache.h
--- a/image/DecodedSurfaceProvider.cpp
+++ b/image/DecodedSurfaceProvider.cpp
@@ -195,16 +195,22 @@ DecodedSurfaceProvider::FinishDecoding()
 {
   mMutex.AssertCurrentThreadOwns();
   MOZ_ASSERT(mImage);
   MOZ_ASSERT(mDecoder);
 
   // Send notifications.
   NotifyDecodeComplete(WrapNotNull(mImage), WrapNotNull(mDecoder));
 
+  // If we have a new and complete surface, we can try to prune similarly sized
+  // surfaces if the cache supports it.
+  if (mSurface && mSurface->IsFinished()) {
+    SurfaceCache::PruneImage(ImageKey(mImage));
+  }
+
   // Destroy our decoder; we don't need it anymore. (And if we don't destroy it,
   // our surface can never be optimized, because the decoder has a
   // RawAccessFrameRef to it.)
   mDecoder = nullptr;
 
   // We don't need a reference to our image anymore, either, and we don't want
   // one. We may be stored in the surface cache for a long time after decoding
   // finishes. If we don't drop our reference to the image, we'll end up
--- a/image/SurfaceCache.cpp
+++ b/image/SurfaceCache.cpp
@@ -156,16 +156,19 @@ public:
     mProvider->SetLocked(aLocked);
   }
 
   bool IsLocked() const
   {
     return !IsPlaceholder() && mIsLocked && mProvider->IsLocked();
   }
 
+  void SetCannotSubstitute() { mProvider->Availability().SetCannotSubstitute(); }
+  bool CannotSubstitute() const { return mProvider->Availability().CannotSubstitute(); }
+
   bool IsPlaceholder() const { return mProvider->Availability().IsPlaceholder(); }
   bool IsDecoded() const { return !IsPlaceholder() && mProvider->IsFinished(); }
 
   ImageKey GetImageKey() const { return mProvider->GetImageKey(); }
   SurfaceKey GetSurfaceKey() const { return mProvider->GetSurfaceKey(); }
   nsExpirationState* GetExpirationState() { return &mExpirationState; }
 
   CostEntry GetCostEntry()
@@ -244,16 +247,17 @@ AreaOfIntSize(const IntSize& aSize) {
  */
 class ImageSurfaceCache
 {
   ~ImageSurfaceCache() { }
 public:
   ImageSurfaceCache()
     : mLocked(false)
     , mFactor2Mode(false)
+    , mFactor2Pruned(false)
   { }
 
   MOZ_DECLARE_REFCOUNTED_TYPENAME(ImageSurfaceCache)
   NS_INLINE_DECL_THREADSAFE_REFCOUNTING(ImageSurfaceCache)
 
   typedef
     nsRefPtrHashtable<nsGenericHashKey<SurfaceKey>, CachedSurface> SurfaceTable;
 
@@ -277,21 +281,27 @@ public:
   }
 
   already_AddRefed<CachedSurface> Lookup(const SurfaceKey& aSurfaceKey,
                                          bool aForAccess)
   {
     RefPtr<CachedSurface> surface;
     mSurfaces.Get(aSurfaceKey, getter_AddRefs(surface));
 
-    // If no exact match is found, and this is for use rather than internal
-    // accounting (i.e. insert and removal), we know this will trigger a
-    // decode. Make sure we switch now to factor of 2 mode if necessary.
-    if (!surface && aForAccess && !mFactor2Mode) {
-      MaybeSetFactor2Mode();
+    if (aForAccess) {
+      if (surface) {
+        // We don't want to allow factor of 2 mode pruning to release surfaces
+        // for which the callers will accept no substitute.
+        surface->SetCannotSubstitute();
+      } else if (!mFactor2Mode) {
+        // If no exact match is found, and this is for use rather than internal
+        // accounting (i.e. insert and removal), we know this will trigger a
+        // decode. Make sure we switch now to factor of 2 mode if necessary.
+        MaybeSetFactor2Mode();
+      }
     }
 
     return surface.forget();
   }
 
   /**
    * @returns A tuple containing the best matching CachedSurface if available,
    *          a MatchType describing how the CachedSurface was selected, and
@@ -460,16 +470,66 @@ public:
         nativeSize.IsEmpty()) {
       return;
     }
 
     // We have a valid size, we can change modes.
     mFactor2Mode = true;
   }
 
+  template<typename Function>
+  void Prune(Function&& aRemoveCallback)
+  {
+    if (!mFactor2Mode || mFactor2Pruned) {
+      return;
+    }
+
+    // Attempt to discard any surfaces which are not factor of 2 and the best
+    // factor of 2 match exists.
+    bool hasNotFactorSize = false;
+    for (auto iter = mSurfaces.Iter(); !iter.Done(); iter.Next()) {
+      NotNull<CachedSurface*> current = WrapNotNull(iter.UserData());
+      const SurfaceKey& currentKey = current->GetSurfaceKey();
+      const IntSize& currentSize = currentKey.Size();
+
+      // First we check if someone requested this size and would not accept
+      // an alternatively sized surface.
+      if (current->CannotSubstitute()) {
+        continue;
+      }
+
+      // Next we find the best factor of 2 size for this surface. If this
+      // surface is a factor of 2 size, then we want to keep it.
+      IntSize bestSize = SuggestedSize(currentSize);
+      if (bestSize == currentSize) {
+        continue;
+      }
+
+      // Check the cache for a surface with the same parameters except for the
+      // size which uses the closest factor of 2 size.
+      SurfaceKey compactKey = currentKey.CloneWithSize(bestSize);
+      RefPtr<CachedSurface> compactMatch;
+      mSurfaces.Get(compactKey, getter_AddRefs(compactMatch));
+      if (compactMatch && compactMatch->IsDecoded()) {
+        aRemoveCallback(current);
+        iter.Remove();
+      } else {
+        hasNotFactorSize = true;
+      }
+    }
+
+    // We have no surfaces that are not factor of 2 sized, so we can stop
+    // pruning henceforth, because we avoid the insertion of new surfaces that
+    // don't match our sizing set (unless the caller won't accept a
+    // substitution.)
+    if (!hasNotFactorSize) {
+      mFactor2Pruned = true;
+    }
+  }
+
   IntSize SuggestedSize(const IntSize& aSize) const
   {
     // When not in factor of 2 mode, we can always decode at the given size.
     if (!mFactor2Mode) {
       return aSize;
     }
 
     MOZ_ASSERT(!IsEmpty());
@@ -548,16 +608,20 @@ public:
 
 private:
   SurfaceTable      mSurfaces;
 
   bool              mLocked;
 
   // True in "factor of 2" mode.
   bool              mFactor2Mode;
+
+  // True if all non-factor of 2 surfaces have been removed from the cache. Note
+  // that this excludes unsubstitutable sizes.
+  bool              mFactor2Pruned;
 };
 
 /**
  * SurfaceCacheImpl is responsible for determining which surfaces will be cached
  * and managing the surface cache data structures. Rather than interact with
  * SurfaceCacheImpl directly, client code interacts with SurfaceCache, which
  * maintains high-level invariants and encapsulates the details of the surface
  * cache's implementation.
@@ -945,16 +1009,28 @@ public:
     // This implicitly unlocks the image if it was locked.
     mImageCaches.Remove(aImageKey);
 
     // Since we did not actually remove any of the surfaces from the cache
     // itself, only stopped tracking them, we should free it outside the lock.
     return cache.forget();
   }
 
+  void PruneImage(const ImageKey aImageKey, const StaticMutexAutoLock& aAutoLock)
+  {
+    RefPtr<ImageSurfaceCache> cache = GetImageCache(aImageKey);
+    if (!cache) {
+      return;  // No cached surfaces for this image, so nothing to do.
+    }
+
+    cache->Prune([this, &aAutoLock](NotNull<CachedSurface*> aSurface) -> void {
+      StopTracking(aSurface, aAutoLock);
+    });
+  }
+
   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(), aAutoLock);
     }
@@ -1395,16 +1471,25 @@ SurfaceCache::RemoveImage(const ImageKey
     StaticMutexAutoLock lock(sInstanceMutex);
     if (sInstance) {
       discard = sInstance->RemoveImage(aImageKey, lock);
     }
   }
 }
 
 /* static */ void
+SurfaceCache::PruneImage(const ImageKey aImageKey)
+{
+  StaticMutexAutoLock lock(sInstanceMutex);
+  if (sInstance) {
+    sInstance->PruneImage(aImageKey, lock);
+  }
+}
+
+/* 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
@@ -400,16 +400,26 @@ struct SurfaceCache
    * entries in the surface cache is destroyed. If another image were allocated
    * at the same address it could result in subtle, difficult-to-reproduce bugs.
    *
    * @param aImageKey  The image which should be removed from the cache.
    */
   static void RemoveImage(const ImageKey aImageKey);
 
   /**
+   * Attempts to remove cache entries (including placeholders) associated with
+   * the given image from the cache, assuming there is an equivalent entry that
+   * 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);
+
+  /**
    * 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();
 
   /**