forked from mirrors/gecko-dev
		
	 cdb1a9e090
			
		
	
	
		cdb1a9e090
		
	
	
	
	
		
			
			If the surface cache entry is a placeholder (because we haven't decoded any frames yet) then a surface cache lookup will not return anything, so we can't get ahold of the AnimationSurfaceProvider to call reset on it. I also looked at the other places we call Lookup on the surface cache and it seems correct to me for them to not get the surface if it's still a placeholder. Differential Revision: https://phabricator.services.mozilla.com/D143894
		
			
				
	
	
		
			1963 lines
		
	
	
	
		
			68 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
			
		
		
	
	
			1963 lines
		
	
	
	
		
			68 KiB
		
	
	
	
		
			C++
		
	
	
	
	
	
| /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
 | |
| /* 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/. */
 | |
| 
 | |
| /**
 | |
|  * SurfaceCache is a service for caching temporary surfaces in imagelib.
 | |
|  */
 | |
| 
 | |
| #include "SurfaceCache.h"
 | |
| 
 | |
| #include <algorithm>
 | |
| #include <utility>
 | |
| 
 | |
| #include "ISurfaceProvider.h"
 | |
| #include "Image.h"
 | |
| #include "LookupResult.h"
 | |
| #include "ShutdownTracker.h"
 | |
| #include "gfx2DGlue.h"
 | |
| #include "gfxPlatform.h"
 | |
| #include "imgFrame.h"
 | |
| #include "mozilla/Assertions.h"
 | |
| #include "mozilla/Attributes.h"
 | |
| #include "mozilla/CheckedInt.h"
 | |
| #include "mozilla/DebugOnly.h"
 | |
| #include "mozilla/Likely.h"
 | |
| #include "mozilla/RefPtr.h"
 | |
| #include "mozilla/StaticMutex.h"
 | |
| #include "mozilla/StaticPrefs_image.h"
 | |
| #include "mozilla/StaticPtr.h"
 | |
| #include "mozilla/Tuple.h"
 | |
| #include "nsExpirationTracker.h"
 | |
| #include "nsHashKeys.h"
 | |
| #include "nsIMemoryReporter.h"
 | |
| #include "nsRefPtrHashtable.h"
 | |
| #include "nsSize.h"
 | |
| #include "nsTArray.h"
 | |
| #include "Orientation.h"
 | |
| #include "prsystem.h"
 | |
| 
 | |
| using std::max;
 | |
| using std::min;
 | |
| 
 | |
| namespace mozilla {
 | |
| 
 | |
| using namespace gfx;
 | |
| 
 | |
| namespace image {
 | |
| 
 | |
| MOZ_DEFINE_MALLOC_SIZE_OF(SurfaceCacheMallocSizeOf)
 | |
| 
 | |
| class CachedSurface;
 | |
| class SurfaceCacheImpl;
 | |
| 
 | |
| ///////////////////////////////////////////////////////////////////////////////
 | |
| // Static Data
 | |
| ///////////////////////////////////////////////////////////////////////////////
 | |
| 
 | |
| // The single surface cache instance.
 | |
| static StaticRefPtr<SurfaceCacheImpl> sInstance;
 | |
| 
 | |
| // The mutex protecting the surface cache.
 | |
| static StaticMutex sInstanceMutex MOZ_UNANNOTATED;
 | |
| 
 | |
| ///////////////////////////////////////////////////////////////////////////////
 | |
| // SurfaceCache Implementation
 | |
| ///////////////////////////////////////////////////////////////////////////////
 | |
| 
 | |
| /**
 | |
|  * Cost models the cost of storing a surface in the cache. Right now, this is
 | |
|  * simply an estimate of the size of the surface in bytes, but in the future it
 | |
|  * may be worth taking into account the cost of rematerializing the surface as
 | |
|  * well.
 | |
|  */
 | |
| typedef size_t Cost;
 | |
| 
 | |
| static Cost ComputeCost(const IntSize& aSize, uint32_t aBytesPerPixel) {
 | |
|   MOZ_ASSERT(aBytesPerPixel == 1 || aBytesPerPixel == 4);
 | |
|   return aSize.width * aSize.height * aBytesPerPixel;
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Since we want to be able to make eviction decisions based on cost, we need to
 | |
|  * be able to look up the CachedSurface which has a certain cost as well as the
 | |
|  * cost associated with a certain CachedSurface. To make this possible, in data
 | |
|  * structures we actually store a CostEntry, which contains a weak pointer to
 | |
|  * its associated surface.
 | |
|  *
 | |
|  * To make usage of the weak pointer safe, SurfaceCacheImpl always calls
 | |
|  * StartTracking after a surface is stored in the cache and StopTracking before
 | |
|  * it is removed.
 | |
|  */
 | |
| class CostEntry {
 | |
|  public:
 | |
|   CostEntry(NotNull<CachedSurface*> aSurface, Cost aCost)
 | |
|       : mSurface(aSurface), mCost(aCost) {}
 | |
| 
 | |
|   NotNull<CachedSurface*> Surface() const { return mSurface; }
 | |
|   Cost GetCost() const { return mCost; }
 | |
| 
 | |
|   bool operator==(const CostEntry& aOther) const {
 | |
|     return mSurface == aOther.mSurface && mCost == aOther.mCost;
 | |
|   }
 | |
| 
 | |
|   bool operator<(const CostEntry& aOther) const {
 | |
|     return mCost < aOther.mCost ||
 | |
|            (mCost == aOther.mCost && mSurface < aOther.mSurface);
 | |
|   }
 | |
| 
 | |
|  private:
 | |
|   NotNull<CachedSurface*> mSurface;
 | |
|   Cost mCost;
 | |
| };
 | |
| 
 | |
| /**
 | |
|  * A CachedSurface associates a surface with a key that uniquely identifies that
 | |
|  * surface.
 | |
|  */
 | |
| class CachedSurface {
 | |
|   ~CachedSurface() {}
 | |
| 
 | |
|  public:
 | |
|   MOZ_DECLARE_REFCOUNTED_TYPENAME(CachedSurface)
 | |
|   NS_INLINE_DECL_THREADSAFE_REFCOUNTING(CachedSurface)
 | |
| 
 | |
|   explicit CachedSurface(NotNull<ISurfaceProvider*> aProvider)
 | |
|       : mProvider(aProvider), mIsLocked(false) {}
 | |
| 
 | |
|   DrawableSurface GetDrawableSurface() const {
 | |
|     if (MOZ_UNLIKELY(IsPlaceholder())) {
 | |
|       MOZ_ASSERT_UNREACHABLE("Called GetDrawableSurface() on a placeholder");
 | |
|       return DrawableSurface();
 | |
|     }
 | |
| 
 | |
|     return mProvider->Surface();
 | |
|   }
 | |
| 
 | |
|   DrawableSurface GetDrawableSurfaceEvenIfPlaceholder() const {
 | |
|     return mProvider->Surface();
 | |
|   }
 | |
| 
 | |
|   void SetLocked(bool aLocked) {
 | |
|     if (IsPlaceholder()) {
 | |
|       return;  // Can't lock a placeholder.
 | |
|     }
 | |
| 
 | |
|     // Update both our state and our provider's state. Some surface providers
 | |
|     // are permanently locked; maintaining our own locking state enables us to
 | |
|     // respect SetLocked() even when it's meaningless from the provider's
 | |
|     // perspective.
 | |
|     mIsLocked = aLocked;
 | |
|     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(); }
 | |
|   const SurfaceKey& GetSurfaceKey() const { return mProvider->GetSurfaceKey(); }
 | |
|   nsExpirationState* GetExpirationState() { return &mExpirationState; }
 | |
| 
 | |
|   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()) {
 | |
|         return;
 | |
|       }
 | |
| 
 | |
|       // Record the memory used by the ISurfaceProvider. This may not have a
 | |
|       // straightforward relationship to the size of the surface that
 | |
|       // DrawableRef() returns if the surface is generated dynamically. (i.e.,
 | |
|       // for surfaces with PlaybackType::eAnimated.)
 | |
|       aCachedSurface->mProvider->AddSizeOfExcludingThis(
 | |
|           mMallocSizeOf, [&](ISurfaceProvider::AddSizeOfCbData& aMetadata) {
 | |
|             SurfaceMemoryCounter counter(aCachedSurface->GetSurfaceKey(),
 | |
|                                          aCachedSurface->IsLocked(),
 | |
|                                          aCachedSurface->CannotSubstitute(),
 | |
|                                          aIsFactor2, aMetadata.mFinished);
 | |
| 
 | |
|             counter.Values().SetDecodedHeap(aMetadata.mHeapBytes);
 | |
|             counter.Values().SetDecodedNonHeap(aMetadata.mNonHeapBytes);
 | |
|             counter.Values().SetDecodedUnknown(aMetadata.mUnknownBytes);
 | |
|             counter.Values().SetExternalHandles(aMetadata.mExternalHandles);
 | |
|             counter.Values().SetFrameIndex(aMetadata.mIndex);
 | |
|             counter.Values().SetExternalId(aMetadata.mExternalId);
 | |
|             counter.Values().SetSurfaceTypes(aMetadata.mTypes);
 | |
| 
 | |
|             mCounters.AppendElement(counter);
 | |
|           });
 | |
|     }
 | |
| 
 | |
|    private:
 | |
|     nsTArray<SurfaceMemoryCounter>& mCounters;
 | |
|     MallocSizeOf mMallocSizeOf;
 | |
|   };
 | |
| 
 | |
|  private:
 | |
|   nsExpirationState mExpirationState;
 | |
|   NotNull<RefPtr<ISurfaceProvider>> mProvider;
 | |
|   bool mIsLocked;
 | |
| };
 | |
| 
 | |
| static int64_t AreaOfIntSize(const IntSize& aSize) {
 | |
|   return static_cast<int64_t>(aSize.width) * static_cast<int64_t>(aSize.height);
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * An ImageSurfaceCache is a per-image surface cache. For correctness we must be
 | |
|  * able to remove all surfaces associated with an image when the image is
 | |
|  * destroyed or invalidated. Since this will happen frequently, it makes sense
 | |
|  * to make it cheap by storing the surfaces for each image separately.
 | |
|  *
 | |
|  * ImageSurfaceCache also keeps track of whether its associated image is locked
 | |
|  * or unlocked.
 | |
|  *
 | |
|  * The cache may also enter "factor of 2" mode which occurs when the number of
 | |
|  * surfaces in the cache exceeds the "image.cache.factor2.threshold-surfaces"
 | |
|  * pref plus the number of native sizes of the image. When in "factor of 2"
 | |
|  * mode, the cache will strongly favour sizes which are a factor of 2 of the
 | |
|  * largest native size. It accomplishes this by suggesting a factor of 2 size
 | |
|  * when lookups fail and substituting the nearest factor of 2 surface to the
 | |
|  * ideal size as the "best" available (as opposed to substitution but not
 | |
|  * found). This allows us to minimize memory consumption and CPU time spent
 | |
|  * decoding when a website requires many variants of the same surface.
 | |
|  */
 | |
| class ImageSurfaceCache {
 | |
|   ~ImageSurfaceCache() {}
 | |
| 
 | |
|  public:
 | |
|   explicit ImageSurfaceCache(const ImageKey aImageKey)
 | |
|       : mLocked(false),
 | |
|         mFactor2Mode(false),
 | |
|         mFactor2Pruned(false),
 | |
|         mIsVectorImage(aImageKey->GetType() == imgIContainer::TYPE_VECTOR) {}
 | |
| 
 | |
|   MOZ_DECLARE_REFCOUNTED_TYPENAME(ImageSurfaceCache)
 | |
|   NS_INLINE_DECL_THREADSAFE_REFCOUNTING(ImageSurfaceCache)
 | |
| 
 | |
|   typedef nsRefPtrHashtable<nsGenericHashKey<SurfaceKey>, CachedSurface>
 | |
|       SurfaceTable;
 | |
| 
 | |
|   auto Values() const { return mSurfaces.Values(); }
 | |
|   uint32_t Count() const { return mSurfaces.Count(); }
 | |
|   bool IsEmpty() const { return mSurfaces.Count() == 0; }
 | |
| 
 | |
|   size_t ShallowSizeOfIncludingThis(MallocSizeOf aMallocSizeOf) const {
 | |
|     size_t bytes = aMallocSizeOf(this) +
 | |
|                    mSurfaces.ShallowSizeOfExcludingThis(aMallocSizeOf);
 | |
|     for (const auto& value : Values()) {
 | |
|       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");
 | |
|     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));
 | |
|     AfterMaybeRemove();
 | |
|     return surface.forget();
 | |
|   }
 | |
| 
 | |
|   already_AddRefed<CachedSurface> Lookup(const SurfaceKey& aSurfaceKey,
 | |
|                                          bool aForAccess) {
 | |
|     RefPtr<CachedSurface> surface;
 | |
|     mSurfaces.Get(aSurfaceKey, getter_AddRefs(surface));
 | |
| 
 | |
|     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
 | |
|    *          an IntSize which is the size the caller should choose to decode
 | |
|    *          at should it attempt to do so.
 | |
|    */
 | |
|   Tuple<already_AddRefed<CachedSurface>, MatchType, IntSize> LookupBestMatch(
 | |
|       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.
 | |
|     IntSize suggestedSize = SuggestedSize(aIdealKey.Size());
 | |
|     if (suggestedSize != aIdealKey.Size()) {
 | |
|       if (!exactMatch) {
 | |
|         SurfaceKey compactKey = aIdealKey.CloneWithSize(suggestedSize);
 | |
|         mSurfaces.Get(compactKey, getter_AddRefs(exactMatch));
 | |
|         if (exactMatch && exactMatch->IsDecoded()) {
 | |
|           MOZ_ASSERT(suggestedSize != aIdealKey.Size());
 | |
|           return MakeTuple(exactMatch.forget(),
 | |
|                            MatchType::SUBSTITUTE_BECAUSE_BEST, suggestedSize);
 | |
|         }
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     // 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 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.
 | |
|       if (currentKey.Flags() != aIdealKey.Flags()) {
 | |
|         continue;
 | |
|       }
 | |
|       // Anything is better than nothing! (Within the constraints we just
 | |
|       // checked, of course.)
 | |
|       if (!bestMatch) {
 | |
|         bestMatch = current;
 | |
|         continue;
 | |
|       }
 | |
| 
 | |
|       MOZ_ASSERT(bestMatch, "Should have a current best match");
 | |
| 
 | |
|       // Always prefer completely decoded surfaces.
 | |
|       bool bestMatchIsDecoded = bestMatch->IsDecoded();
 | |
|       if (bestMatchIsDecoded && !current->IsDecoded()) {
 | |
|         continue;
 | |
|       }
 | |
|       if (!bestMatchIsDecoded && current->IsDecoded()) {
 | |
|         bestMatch = current;
 | |
|         continue;
 | |
|       }
 | |
| 
 | |
|       SurfaceKey bestMatchKey = bestMatch->GetSurfaceKey();
 | |
|       if (CompareArea(aIdealKey.Size(), bestMatchKey.Size(),
 | |
|                       currentKey.Size())) {
 | |
|         bestMatch = current;
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     MatchType matchType;
 | |
|     if (bestMatch) {
 | |
|       if (!exactMatch) {
 | |
|         // No exact match, neither ideal nor factor of 2.
 | |
|         MOZ_ASSERT(suggestedSize != bestMatch->GetSurfaceKey().Size(),
 | |
|                    "No exact match despite the fact the sizes match!");
 | |
|         matchType = MatchType::SUBSTITUTE_BECAUSE_NOT_FOUND;
 | |
|       } else if (exactMatch != bestMatch) {
 | |
|         // The exact match is still decoding, but we found a substitute.
 | |
|         matchType = MatchType::SUBSTITUTE_BECAUSE_PENDING;
 | |
|       } else if (aIdealKey.Size() != bestMatch->GetSurfaceKey().Size()) {
 | |
|         // The best factor of 2 match is still decoding, but the best we've got.
 | |
|         MOZ_ASSERT(suggestedSize != aIdealKey.Size());
 | |
|         MOZ_ASSERT(mFactor2Mode || mIsVectorImage);
 | |
|         matchType = MatchType::SUBSTITUTE_BECAUSE_BEST;
 | |
|       } else {
 | |
|         // The exact match is still decoding, but it's the best we've got.
 | |
|         matchType = MatchType::EXACT;
 | |
|       }
 | |
|     } else {
 | |
|       if (exactMatch) {
 | |
|         // We found an "exact match"; it must have been a placeholder.
 | |
|         MOZ_ASSERT(exactMatch->IsPlaceholder());
 | |
|         matchType = MatchType::PENDING;
 | |
|       } else {
 | |
|         // We couldn't find an exact match *or* a substitute.
 | |
|         matchType = MatchType::NOT_FOUND;
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     return MakeTuple(bestMatch.forget(), matchType, suggestedSize);
 | |
|   }
 | |
| 
 | |
|   void MaybeSetFactor2Mode() {
 | |
|     MOZ_ASSERT(!mFactor2Mode);
 | |
| 
 | |
|     // Typically an image cache will not have too many size-varying surfaces, so
 | |
|     // if we exceed the given threshold, we should consider using a subset.
 | |
|     int32_t thresholdSurfaces =
 | |
|         StaticPrefs::image_cache_factor2_threshold_surfaces();
 | |
|     if (thresholdSurfaces < 0 ||
 | |
|         mSurfaces.Count() <= static_cast<uint32_t>(thresholdSurfaces)) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     // Determine how many native surfaces this image has. If it is zero, and it
 | |
|     // is a vector image, then we should impute a single native size. Otherwise,
 | |
|     // it may be zero because we don't know yet, or the image has an error, or
 | |
|     // it isn't supported.
 | |
|     NotNull<CachedSurface*> current =
 | |
|         WrapNotNull(mSurfaces.ConstIter().UserData());
 | |
|     Image* image = static_cast<Image*>(current->GetImageKey());
 | |
|     size_t nativeSizes = image->GetNativeSizesLength();
 | |
|     if (mIsVectorImage) {
 | |
|       MOZ_ASSERT(nativeSizes == 0);
 | |
|       nativeSizes = 1;
 | |
|     } else if (nativeSizes == 0) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     // Increase the threshold by the number of native sizes. This ensures that
 | |
|     // we do not prevent decoding of the image at all its native sizes. It does
 | |
|     // not guarantee we will provide a surface at that size however (i.e. many
 | |
|     // other sized surfaces are requested, in addition to the native sizes).
 | |
|     thresholdSurfaces += nativeSizes;
 | |
|     if (mSurfaces.Count() <= static_cast<uint32_t>(thresholdSurfaces)) {
 | |
|       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;
 | |
|     }
 | |
| 
 | |
|     // 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;
 | |
|   }
 | |
| 
 | |
|   IntSize SuggestedSizeInternal(const IntSize& aSize) const {
 | |
|     // When not in factor of 2 mode, we can always decode at the given size.
 | |
|     if (!mFactor2Mode) {
 | |
|       return aSize;
 | |
|     }
 | |
| 
 | |
|     // We cannot enter factor of 2 mode unless we have a minimum number of
 | |
|     // surfaces, and we should have left it if the cache was emptied.
 | |
|     if (MOZ_UNLIKELY(IsEmpty())) {
 | |
|       MOZ_ASSERT_UNREACHABLE("Should not be empty and in factor of 2 mode!");
 | |
|       return aSize;
 | |
|     }
 | |
| 
 | |
|     // This bit of awkwardness gets the largest native size of the image.
 | |
|     NotNull<CachedSurface*> firstSurface =
 | |
|         WrapNotNull(mSurfaces.ConstIter().UserData());
 | |
|     Image* image = static_cast<Image*>(firstSurface->GetImageKey());
 | |
|     IntSize factorSize;
 | |
|     if (NS_FAILED(image->GetWidth(&factorSize.width)) ||
 | |
|         NS_FAILED(image->GetHeight(&factorSize.height)) ||
 | |
|         factorSize.IsEmpty()) {
 | |
|       // Valid vector images may have a default size of 0x0. In that case, just
 | |
|       // assume a default size of 100x100 and apply the intrinsic ratio if
 | |
|       // available. If our guess was too small, don't use factor-of-scaling.
 | |
|       MOZ_ASSERT(mIsVectorImage);
 | |
|       factorSize = IntSize(100, 100);
 | |
|       Maybe<AspectRatio> aspectRatio = image->GetIntrinsicRatio();
 | |
|       if (aspectRatio && *aspectRatio) {
 | |
|         factorSize.width =
 | |
|             NSToIntRound(aspectRatio->ApplyToFloat(float(factorSize.height)));
 | |
|         if (factorSize.IsEmpty()) {
 | |
|           return aSize;
 | |
|         }
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     if (mIsVectorImage) {
 | |
|       // Ensure the aspect ratio matches the native size before forcing the
 | |
|       // caller to accept a factor of 2 size. The difference between the aspect
 | |
|       // ratios is:
 | |
|       //
 | |
|       //     delta = nativeWidth/nativeHeight - desiredWidth/desiredHeight
 | |
|       //
 | |
|       //     delta*nativeHeight*desiredHeight = nativeWidth*desiredHeight
 | |
|       //                                      - desiredWidth*nativeHeight
 | |
|       //
 | |
|       // Using the maximum accepted delta as a constant, we can avoid the
 | |
|       // floating point division and just compare after some integer ops.
 | |
|       int32_t delta =
 | |
|           factorSize.width * aSize.height - aSize.width * factorSize.height;
 | |
|       int32_t maxDelta = (factorSize.height * aSize.height) >> 4;
 | |
|       if (delta > maxDelta || delta < -maxDelta) {
 | |
|         return aSize;
 | |
|       }
 | |
| 
 | |
|       // If the requested size is bigger than the native size, we actually need
 | |
|       // to grow the native size instead of shrinking it.
 | |
|       if (factorSize.width < aSize.width) {
 | |
|         do {
 | |
|           IntSize candidate(factorSize.width * 2, factorSize.height * 2);
 | |
|           if (!SurfaceCache::IsLegalSize(candidate)) {
 | |
|             break;
 | |
|           }
 | |
| 
 | |
|           factorSize = candidate;
 | |
|         } while (factorSize.width < aSize.width);
 | |
| 
 | |
|         return factorSize;
 | |
|       }
 | |
| 
 | |
|       // Otherwise we can find the best fit as normal.
 | |
|     }
 | |
| 
 | |
|     // Start with the native size as the best first guess.
 | |
|     IntSize bestSize = factorSize;
 | |
|     factorSize.width /= 2;
 | |
|     factorSize.height /= 2;
 | |
| 
 | |
|     while (!factorSize.IsEmpty()) {
 | |
|       if (!CompareArea(aSize, bestSize, factorSize)) {
 | |
|         // This size is not better than the last. Since we proceed from largest
 | |
|         // to smallest, we know that the next size will not be better if the
 | |
|         // previous size was rejected. Break early.
 | |
|         break;
 | |
|       }
 | |
| 
 | |
|       // The current factor of 2 size is better than the last selected size.
 | |
|       bestSize = factorSize;
 | |
|       factorSize.width /= 2;
 | |
|       factorSize.height /= 2;
 | |
|     }
 | |
| 
 | |
|     return bestSize;
 | |
|   }
 | |
| 
 | |
|   bool CompareArea(const IntSize& aIdealSize, const IntSize& aBestSize,
 | |
|                    const IntSize& aSize) const {
 | |
|     // Compare sizes. We use an area-based heuristic here instead of computing a
 | |
|     // truly optimal answer, since it seems very unlikely to make a difference
 | |
|     // for realistic sizes.
 | |
|     int64_t idealArea = AreaOfIntSize(aIdealSize);
 | |
|     int64_t currentArea = AreaOfIntSize(aSize);
 | |
|     int64_t bestMatchArea = AreaOfIntSize(aBestSize);
 | |
| 
 | |
|     // If the best match is smaller than the ideal size, prefer bigger sizes.
 | |
|     if (bestMatchArea < idealArea) {
 | |
|       if (currentArea > bestMatchArea) {
 | |
|         return true;
 | |
|       }
 | |
|       return false;
 | |
|     }
 | |
| 
 | |
|     // Other, prefer sizes closer to the ideal size, but still not smaller.
 | |
|     if (idealArea <= currentArea && currentArea < bestMatchArea) {
 | |
|       return true;
 | |
|     }
 | |
| 
 | |
|     // This surface isn't an improvement over the current best match.
 | |
|     return false;
 | |
|   }
 | |
| 
 | |
|   template <typename Function>
 | |
|   void CollectSizeOfSurfaces(nsTArray<SurfaceMemoryCounter>& aCounters,
 | |
|                              MallocSizeOf aMallocSizeOf,
 | |
|                              Function&& aRemoveCallback) {
 | |
|     CachedSurface::SurfaceMemoryReport report(aCounters, aMallocSizeOf);
 | |
|     for (auto iter = mSurfaces.Iter(); !iter.Done(); iter.Next()) {
 | |
|       NotNull<CachedSurface*> surface = WrapNotNull(iter.UserData());
 | |
| 
 | |
|       // We don't need the drawable surface for ourselves, but adding a surface
 | |
|       // to the report will trigger this indirectly. If the surface was
 | |
|       // discarded by the OS because it was in volatile memory, we should remove
 | |
|       // it from the cache immediately rather than include it in the report.
 | |
|       DrawableSurface drawableSurface;
 | |
|       if (!surface->IsPlaceholder()) {
 | |
|         drawableSurface = surface->GetDrawableSurface();
 | |
|         if (!drawableSurface) {
 | |
|           aRemoveCallback(surface);
 | |
|           iter.Remove();
 | |
|           continue;
 | |
|         }
 | |
|       }
 | |
| 
 | |
|       const IntSize& size = surface->GetSurfaceKey().Size();
 | |
|       bool factor2Size = false;
 | |
|       if (mFactor2Mode) {
 | |
|         factor2Size = (size == SuggestedSize(size));
 | |
|       }
 | |
|       report.Add(surface, factor2Size);
 | |
|     }
 | |
| 
 | |
|     AfterMaybeRemove();
 | |
|   }
 | |
| 
 | |
|   void SetLocked(bool aLocked) { mLocked = aLocked; }
 | |
|   bool IsLocked() const { return mLocked; }
 | |
| 
 | |
|  private:
 | |
|   void AfterMaybeRemove() {
 | |
|     if (IsEmpty() && mFactor2Mode) {
 | |
|       // The last surface for this cache was removed. This can happen if the
 | |
|       // surface was stored in a volatile buffer and got purged, or the surface
 | |
|       // expired from the cache. If the cache itself lingers for some reason
 | |
|       // (e.g. in the process of performing a lookup, the cache itself is
 | |
|       // locked), then we need to reset the factor of 2 state because it
 | |
|       // requires at least one surface present to get the native size
 | |
|       // information from the image.
 | |
|       mFactor2Mode = mFactor2Pruned = false;
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   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;
 | |
| 
 | |
|   // True if the surfaces are produced from a vector image. If so, it must match
 | |
|   // the aspect ratio when using factor of 2 mode.
 | |
|   bool mIsVectorImage;
 | |
| };
 | |
| 
 | |
| /**
 | |
|  * 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.
 | |
|  */
 | |
| class SurfaceCacheImpl final : public nsIMemoryReporter {
 | |
|  public:
 | |
|   NS_DECL_ISUPPORTS
 | |
| 
 | |
|   SurfaceCacheImpl(uint32_t aSurfaceCacheExpirationTimeMS,
 | |
|                    uint32_t aSurfaceCacheDiscardFactor,
 | |
|                    uint32_t aSurfaceCacheSize)
 | |
|       : mExpirationTracker(aSurfaceCacheExpirationTimeMS),
 | |
|         mMemoryPressureObserver(new MemoryPressureObserver),
 | |
|         mDiscardFactor(aSurfaceCacheDiscardFactor),
 | |
|         mMaxCost(aSurfaceCacheSize),
 | |
|         mAvailableCost(aSurfaceCacheSize),
 | |
|         mLockedCost(0),
 | |
|         mOverflowCount(0),
 | |
|         mAlreadyPresentCount(0),
 | |
|         mTableFailureCount(0),
 | |
|         mTrackingFailureCount(0) {
 | |
|     nsCOMPtr<nsIObserverService> os = services::GetObserverService();
 | |
|     if (os) {
 | |
|       os->AddObserver(mMemoryPressureObserver, "memory-pressure", false);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|  private:
 | |
|   virtual ~SurfaceCacheImpl() {
 | |
|     nsCOMPtr<nsIObserverService> os = services::GetObserverService();
 | |
|     if (os) {
 | |
|       os->RemoveObserver(mMemoryPressureObserver, "memory-pressure");
 | |
|     }
 | |
| 
 | |
|     UnregisterWeakMemoryReporter(this);
 | |
|   }
 | |
| 
 | |
|  public:
 | |
|   void InitMemoryReporter() { RegisterWeakMemoryReporter(this); }
 | |
| 
 | |
|   InsertOutcome Insert(NotNull<ISurfaceProvider*> aProvider, bool aSetAvailable,
 | |
|                        const StaticMutexAutoLock& aAutoLock) {
 | |
|     // If this is a duplicate surface, refuse to replace the original.
 | |
|     // XXX(seth): Calling Lookup() and then RemoveEntry() does the lookup
 | |
|     // twice. We'll make this more efficient in bug 1185137.
 | |
|     LookupResult result =
 | |
|         Lookup(aProvider->GetImageKey(), aProvider->GetSurfaceKey(), aAutoLock,
 | |
|                /* aMarkUsed = */ false);
 | |
|     if (MOZ_UNLIKELY(result)) {
 | |
|       mAlreadyPresentCount++;
 | |
|       return InsertOutcome::FAILURE_ALREADY_PRESENT;
 | |
|     }
 | |
| 
 | |
|     if (result.Type() == MatchType::PENDING) {
 | |
|       RemoveEntry(aProvider->GetImageKey(), aProvider->GetSurfaceKey(),
 | |
|                   aAutoLock);
 | |
|     }
 | |
| 
 | |
|     MOZ_ASSERT(result.Type() == MatchType::NOT_FOUND ||
 | |
|                    result.Type() == MatchType::PENDING,
 | |
|                "A LookupResult with no surface should be NOT_FOUND or PENDING");
 | |
| 
 | |
|     // If this is bigger than we can hold after discarding everything we can,
 | |
|     // refuse to cache it.
 | |
|     Cost cost = aProvider->LogicalSizeInBytes();
 | |
|     if (MOZ_UNLIKELY(!CanHoldAfterDiscarding(cost))) {
 | |
|       mOverflowCount++;
 | |
|       return InsertOutcome::FAILURE;
 | |
|     }
 | |
| 
 | |
|     // Remove elements in order of cost until we can fit this in the cache. Note
 | |
|     // that locked surfaces aren't in mCosts, so we never remove them here.
 | |
|     while (cost > mAvailableCost) {
 | |
|       MOZ_ASSERT(!mCosts.IsEmpty(),
 | |
|                  "Removed everything and it still won't fit");
 | |
|       Remove(mCosts.LastElement().Surface(), /* aStopTracking */ true,
 | |
|              aAutoLock);
 | |
|     }
 | |
| 
 | |
|     // Locate the appropriate per-image cache. If there's not an existing cache
 | |
|     // for this image, create it.
 | |
|     const ImageKey imageKey = aProvider->GetImageKey();
 | |
|     RefPtr<ImageSurfaceCache> cache = GetImageCache(imageKey);
 | |
|     if (!cache) {
 | |
|       cache = new ImageSurfaceCache(imageKey);
 | |
|       if (!mImageCaches.InsertOrUpdate(aProvider->GetImageKey(), RefPtr{cache},
 | |
|                                        fallible)) {
 | |
|         mTableFailureCount++;
 | |
|         return InsertOutcome::FAILURE;
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     // If we were asked to mark the cache entry available, do so.
 | |
|     if (aSetAvailable) {
 | |
|       aProvider->Availability().SetAvailable();
 | |
|     }
 | |
| 
 | |
|     auto surface = MakeNotNull<RefPtr<CachedSurface>>(aProvider);
 | |
| 
 | |
|     // We require that locking succeed if the image is locked and we're not
 | |
|     // inserting a placeholder; the caller may need to know this to handle
 | |
|     // errors correctly.
 | |
|     bool mustLock = cache->IsLocked() && !surface->IsPlaceholder();
 | |
|     if (mustLock) {
 | |
|       surface->SetLocked(true);
 | |
|       if (!surface->IsLocked()) {
 | |
|         return InsertOutcome::FAILURE;
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     // Insert.
 | |
|     MOZ_ASSERT(cost <= mAvailableCost, "Inserting despite too large a cost");
 | |
|     if (!cache->Insert(surface)) {
 | |
|       mTableFailureCount++;
 | |
|       if (mustLock) {
 | |
|         surface->SetLocked(false);
 | |
|       }
 | |
|       return InsertOutcome::FAILURE;
 | |
|     }
 | |
| 
 | |
|     if (MOZ_UNLIKELY(!StartTracking(surface, aAutoLock))) {
 | |
|       MOZ_ASSERT(!mustLock);
 | |
|       Remove(surface, /* aStopTracking */ false, aAutoLock);
 | |
|       return InsertOutcome::FAILURE;
 | |
|     }
 | |
| 
 | |
|     return InsertOutcome::SUCCESS;
 | |
|   }
 | |
| 
 | |
|   void Remove(NotNull<CachedSurface*> aSurface, bool aStopTracking,
 | |
|               const StaticMutexAutoLock& aAutoLock) {
 | |
|     ImageKey imageKey = aSurface->GetImageKey();
 | |
| 
 | |
|     RefPtr<ImageSurfaceCache> cache = GetImageCache(imageKey);
 | |
|     MOZ_ASSERT(cache, "Shouldn't try to remove a surface with no image cache");
 | |
| 
 | |
|     // If the surface was not a placeholder, tell its image that we discarded
 | |
|     // it.
 | |
|     if (!aSurface->IsPlaceholder()) {
 | |
|       static_cast<Image*>(imageKey)->OnSurfaceDiscarded(
 | |
|           aSurface->GetSurfaceKey());
 | |
|     }
 | |
| 
 | |
|     // If we failed during StartTracking, we can skip this step.
 | |
|     if (aStopTracking) {
 | |
|       StopTracking(aSurface, /* aIsTracked */ true, aAutoLock);
 | |
|     }
 | |
| 
 | |
|     // Individual surfaces must be freed outside the lock.
 | |
|     mCachedSurfacesDiscard.AppendElement(cache->Remove(aSurface));
 | |
| 
 | |
|     MaybeRemoveEmptyCache(imageKey, cache);
 | |
|   }
 | |
| 
 | |
|   bool StartTracking(NotNull<CachedSurface*> aSurface,
 | |
|                      const StaticMutexAutoLock& aAutoLock) {
 | |
|     CostEntry costEntry = aSurface->GetCostEntry();
 | |
|     MOZ_ASSERT(costEntry.GetCost() <= mAvailableCost,
 | |
|                "Cost too large and the caller didn't catch it");
 | |
| 
 | |
|     if (aSurface->IsLocked()) {
 | |
|       mLockedCost += costEntry.GetCost();
 | |
|       MOZ_ASSERT(mLockedCost <= mMaxCost, "Locked more than we can hold?");
 | |
|     } else {
 | |
|       if (NS_WARN_IF(!mCosts.InsertElementSorted(costEntry, fallible))) {
 | |
|         mTrackingFailureCount++;
 | |
|         return false;
 | |
|       }
 | |
| 
 | |
|       // This may fail during XPCOM shutdown, so we need to ensure the object is
 | |
|       // tracked before calling RemoveObject in StopTracking.
 | |
|       nsresult rv = mExpirationTracker.AddObjectLocked(aSurface, aAutoLock);
 | |
|       if (NS_WARN_IF(NS_FAILED(rv))) {
 | |
|         DebugOnly<bool> foundInCosts = mCosts.RemoveElementSorted(costEntry);
 | |
|         MOZ_ASSERT(foundInCosts, "Lost track of costs for this surface");
 | |
|         mTrackingFailureCount++;
 | |
|         return false;
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     mAvailableCost -= costEntry.GetCost();
 | |
|     return true;
 | |
|   }
 | |
| 
 | |
|   void StopTracking(NotNull<CachedSurface*> aSurface, bool aIsTracked,
 | |
|                     const StaticMutexAutoLock& aAutoLock) {
 | |
|     CostEntry costEntry = aSurface->GetCostEntry();
 | |
| 
 | |
|     if (aSurface->IsLocked()) {
 | |
|       MOZ_ASSERT(mLockedCost >= costEntry.GetCost(), "Costs don't balance");
 | |
|       mLockedCost -= costEntry.GetCost();
 | |
|       // XXX(seth): It'd be nice to use an O(log n) lookup here. This is O(n).
 | |
|       MOZ_ASSERT(!mCosts.Contains(costEntry),
 | |
|                  "Shouldn't have a cost entry for a locked surface");
 | |
|     } else {
 | |
|       if (MOZ_LIKELY(aSurface->GetExpirationState()->IsTracked())) {
 | |
|         MOZ_ASSERT(aIsTracked, "Expiration-tracking a surface unexpectedly!");
 | |
|         mExpirationTracker.RemoveObjectLocked(aSurface, aAutoLock);
 | |
|       } else {
 | |
|         // Our call to AddObject must have failed in StartTracking; most likely
 | |
|         // we're in XPCOM shutdown right now.
 | |
|         MOZ_ASSERT(!aIsTracked, "Not expiration-tracking an unlocked surface!");
 | |
|       }
 | |
| 
 | |
|       DebugOnly<bool> foundInCosts = mCosts.RemoveElementSorted(costEntry);
 | |
|       MOZ_ASSERT(foundInCosts, "Lost track of costs for this surface");
 | |
|     }
 | |
| 
 | |
|     mAvailableCost += costEntry.GetCost();
 | |
|     MOZ_ASSERT(mAvailableCost <= mMaxCost,
 | |
|                "More available cost than we started with");
 | |
|   }
 | |
| 
 | |
|   LookupResult Lookup(const ImageKey aImageKey, const SurfaceKey& aSurfaceKey,
 | |
|                       const StaticMutexAutoLock& aAutoLock, bool aMarkUsed) {
 | |
|     RefPtr<ImageSurfaceCache> cache = GetImageCache(aImageKey);
 | |
|     if (!cache) {
 | |
|       // No cached surfaces for this image.
 | |
|       return LookupResult(MatchType::NOT_FOUND);
 | |
|     }
 | |
| 
 | |
|     RefPtr<CachedSurface> surface = cache->Lookup(aSurfaceKey, aMarkUsed);
 | |
|     if (!surface) {
 | |
|       // Lookup in the per-image cache missed.
 | |
|       return LookupResult(MatchType::NOT_FOUND);
 | |
|     }
 | |
| 
 | |
|     if (surface->IsPlaceholder()) {
 | |
|       return LookupResult(MatchType::PENDING);
 | |
|     }
 | |
| 
 | |
|     DrawableSurface drawableSurface = surface->GetDrawableSurface();
 | |
|     if (!drawableSurface) {
 | |
|       // The surface was released by the operating system. Remove the cache
 | |
|       // entry as well.
 | |
|       Remove(WrapNotNull(surface), /* aStopTracking */ true, aAutoLock);
 | |
|       return LookupResult(MatchType::NOT_FOUND);
 | |
|     }
 | |
| 
 | |
|     if (aMarkUsed &&
 | |
|         !MarkUsed(WrapNotNull(surface), WrapNotNull(cache), aAutoLock)) {
 | |
|       Remove(WrapNotNull(surface), /* aStopTracking */ false, aAutoLock);
 | |
|       return LookupResult(MatchType::NOT_FOUND);
 | |
|     }
 | |
| 
 | |
|     MOZ_ASSERT(surface->GetSurfaceKey() == aSurfaceKey,
 | |
|                "Lookup() not returning an exact match?");
 | |
|     return LookupResult(std::move(drawableSurface), MatchType::EXACT);
 | |
|   }
 | |
| 
 | |
|   LookupResult LookupBestMatch(const ImageKey aImageKey,
 | |
|                                const SurfaceKey& aSurfaceKey,
 | |
|                                const StaticMutexAutoLock& aAutoLock,
 | |
|                                bool aMarkUsed) {
 | |
|     RefPtr<ImageSurfaceCache> cache = GetImageCache(aImageKey);
 | |
|     if (!cache) {
 | |
|       // No cached surfaces for this image.
 | |
|       return LookupResult(
 | |
|           MatchType::NOT_FOUND,
 | |
|           SurfaceCache::ClampSize(aImageKey, aSurfaceKey.Size()));
 | |
|     }
 | |
| 
 | |
|     // Repeatedly look up the best match, trying again if the resulting surface
 | |
|     // has been freed by the operating system, until we can either lock a
 | |
|     // surface for drawing or there are no matching surfaces left.
 | |
|     // XXX(seth): This is O(N^2), but N is expected to be very small. If we
 | |
|     // encounter a performance problem here we can revisit this.
 | |
| 
 | |
|     RefPtr<CachedSurface> surface;
 | |
|     DrawableSurface drawableSurface;
 | |
|     MatchType matchType = MatchType::NOT_FOUND;
 | |
|     IntSize suggestedSize;
 | |
|     while (true) {
 | |
|       Tie(surface, matchType, suggestedSize) =
 | |
|           cache->LookupBestMatch(aSurfaceKey);
 | |
| 
 | |
|       if (!surface) {
 | |
|         return LookupResult(
 | |
|             matchType, suggestedSize);  // Lookup in the per-image cache missed.
 | |
|       }
 | |
| 
 | |
|       drawableSurface = surface->GetDrawableSurface();
 | |
|       if (drawableSurface) {
 | |
|         break;
 | |
|       }
 | |
| 
 | |
|       // The surface was released by the operating system. Remove the cache
 | |
|       // entry as well.
 | |
|       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().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);
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     return LookupResult(std::move(drawableSurface), matchType, suggestedSize);
 | |
|   }
 | |
| 
 | |
|   bool CanHold(const Cost aCost) const { return aCost <= mMaxCost; }
 | |
| 
 | |
|   size_t MaximumCapacity() const { return size_t(mMaxCost); }
 | |
| 
 | |
|   void SurfaceAvailable(NotNull<ISurfaceProvider*> aProvider,
 | |
|                         const StaticMutexAutoLock& aAutoLock) {
 | |
|     if (!aProvider->Availability().IsPlaceholder()) {
 | |
|       MOZ_ASSERT_UNREACHABLE("Calling SurfaceAvailable on non-placeholder");
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     // Reinsert the provider, requesting that Insert() mark it available. This
 | |
|     // may or may not succeed, depending on whether some other decoder has
 | |
|     // beaten us to the punch and inserted a non-placeholder version of this
 | |
|     // surface first, but it's fine either way.
 | |
|     // XXX(seth): This could be implemented more efficiently; we should be able
 | |
|     // to just update our data structures without reinserting.
 | |
|     Insert(aProvider, /* aSetAvailable = */ true, aAutoLock);
 | |
|   }
 | |
| 
 | |
|   void LockImage(const ImageKey aImageKey) {
 | |
|     RefPtr<ImageSurfaceCache> cache = GetImageCache(aImageKey);
 | |
|     if (!cache) {
 | |
|       cache = new ImageSurfaceCache(aImageKey);
 | |
|       mImageCaches.InsertOrUpdate(aImageKey, RefPtr{cache});
 | |
|     }
 | |
| 
 | |
|     cache->SetLocked(true);
 | |
| 
 | |
|     // We don't relock this image's existing surfaces right away; instead, the
 | |
|     // image should arrange for Lookup() to touch them if they are still useful.
 | |
|   }
 | |
| 
 | |
|   void UnlockImage(const ImageKey aImageKey,
 | |
|                    const StaticMutexAutoLock& aAutoLock) {
 | |
|     RefPtr<ImageSurfaceCache> cache = GetImageCache(aImageKey);
 | |
|     if (!cache || !cache->IsLocked()) {
 | |
|       return;  // Already unlocked.
 | |
|     }
 | |
| 
 | |
|     cache->SetLocked(false);
 | |
|     DoUnlockSurfaces(WrapNotNull(cache), /* aStaticOnly = */ false, aAutoLock);
 | |
|   }
 | |
| 
 | |
|   void UnlockEntries(const ImageKey aImageKey,
 | |
|                      const StaticMutexAutoLock& aAutoLock) {
 | |
|     RefPtr<ImageSurfaceCache> cache = GetImageCache(aImageKey);
 | |
|     if (!cache || !cache->IsLocked()) {
 | |
|       return;  // Already unlocked.
 | |
|     }
 | |
| 
 | |
|     // (Note that we *don't* unlock the per-image cache here; that's the
 | |
|     // difference between this and UnlockImage.)
 | |
|     DoUnlockSurfaces(WrapNotNull(cache),
 | |
|                      /* aStaticOnly = */
 | |
|                      !StaticPrefs::image_mem_animated_discardable_AtStartup(),
 | |
|                      aAutoLock);
 | |
|   }
 | |
| 
 | |
|   already_AddRefed<ImageSurfaceCache> RemoveImage(
 | |
|       const ImageKey aImageKey, const StaticMutexAutoLock& aAutoLock) {
 | |
|     RefPtr<ImageSurfaceCache> cache = GetImageCache(aImageKey);
 | |
|     if (!cache) {
 | |
|       return nullptr;  // No cached surfaces for this image, so nothing to do.
 | |
|     }
 | |
| 
 | |
|     // Discard all of the cached surfaces for this image.
 | |
|     // XXX(seth): This is O(n^2) since for each item in the cache we are
 | |
|     // removing an element from the costs array. Since n is expected to be
 | |
|     // small, performance should be good, but if usage patterns change we should
 | |
|     // change the data structure used for mCosts.
 | |
|     for (const auto& value : cache->Values()) {
 | |
|       StopTracking(WrapNotNull(value),
 | |
|                    /* aIsTracked */ true, aAutoLock);
 | |
|     }
 | |
| 
 | |
|     // The per-image cache isn't needed anymore, so remove it as well.
 | |
|     // 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, /* 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);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   void DiscardForMemoryPressure(const StaticMutexAutoLock& aAutoLock) {
 | |
|     // Compute our discardable cost. Since locked surfaces aren't discardable,
 | |
|     // we exclude them.
 | |
|     const Cost discardableCost = (mMaxCost - mAvailableCost) - mLockedCost;
 | |
|     MOZ_ASSERT(discardableCost <= mMaxCost, "Discardable cost doesn't add up");
 | |
| 
 | |
|     // Our target is to raise our available cost by (1 / mDiscardFactor) of our
 | |
|     // discardable cost - in other words, we want to end up with about
 | |
|     // (discardableCost / mDiscardFactor) fewer bytes stored in the surface
 | |
|     // cache after we're done.
 | |
|     const Cost targetCost = mAvailableCost + (discardableCost / mDiscardFactor);
 | |
| 
 | |
|     if (targetCost > mMaxCost - mLockedCost) {
 | |
|       MOZ_ASSERT_UNREACHABLE("Target cost is more than we can discard");
 | |
|       DiscardAll(aAutoLock);
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     // Discard surfaces until we've reduced our cost to our target cost.
 | |
|     while (mAvailableCost < targetCost) {
 | |
|       MOZ_ASSERT(!mCosts.IsEmpty(), "Removed everything and still not done");
 | |
|       Remove(mCosts.LastElement().Surface(), /* aStopTracking */ true,
 | |
|              aAutoLock);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   void TakeDiscard(nsTArray<RefPtr<CachedSurface>>& aDiscard,
 | |
|                    const StaticMutexAutoLock& aAutoLock) {
 | |
|     MOZ_ASSERT(aDiscard.IsEmpty());
 | |
|     aDiscard = std::move(mCachedSurfacesDiscard);
 | |
|   }
 | |
| 
 | |
|   already_AddRefed<CachedSurface> GetSurfaceForResetAnimation(
 | |
|       const ImageKey aImageKey, const SurfaceKey& aSurfaceKey,
 | |
|       const StaticMutexAutoLock& aAutoLock) {
 | |
|     RefPtr<CachedSurface> surface;
 | |
| 
 | |
|     RefPtr<ImageSurfaceCache> cache = GetImageCache(aImageKey);
 | |
|     if (!cache) {
 | |
|       // No cached surfaces for this image.
 | |
|       return surface.forget();
 | |
|     }
 | |
| 
 | |
|     surface = cache->Lookup(aSurfaceKey, /* aForAccess = */ false);
 | |
|     return surface.forget();
 | |
|   }
 | |
| 
 | |
|   void LockSurface(NotNull<CachedSurface*> aSurface,
 | |
|                    const StaticMutexAutoLock& aAutoLock) {
 | |
|     if (aSurface->IsPlaceholder() || aSurface->IsLocked()) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     StopTracking(aSurface, /* aIsTracked */ true, aAutoLock);
 | |
| 
 | |
|     // Lock the surface. This can fail.
 | |
|     aSurface->SetLocked(true);
 | |
|     DebugOnly<bool> tracked = StartTracking(aSurface, aAutoLock);
 | |
|     MOZ_ASSERT(tracked);
 | |
|   }
 | |
| 
 | |
|   size_t ShallowSizeOfIncludingThis(
 | |
|       MallocSizeOf aMallocSizeOf, const StaticMutexAutoLock& aAutoLock) const {
 | |
|     size_t bytes =
 | |
|         aMallocSizeOf(this) + mCosts.ShallowSizeOfExcludingThis(aMallocSizeOf) +
 | |
|         mImageCaches.ShallowSizeOfExcludingThis(aMallocSizeOf) +
 | |
|         mCachedSurfacesDiscard.ShallowSizeOfExcludingThis(aMallocSizeOf) +
 | |
|         mExpirationTracker.ShallowSizeOfExcludingThis(aMallocSizeOf);
 | |
|     for (const auto& data : mImageCaches.Values()) {
 | |
|       bytes += data->ShallowSizeOfIncludingThis(aMallocSizeOf);
 | |
|     }
 | |
|     return bytes;
 | |
|   }
 | |
| 
 | |
|   NS_IMETHOD
 | |
|   CollectReports(nsIHandleReportCallback* aHandleReport, nsISupports* aData,
 | |
|                  bool aAnonymize) override {
 | |
|     StaticMutexAutoLock lock(sInstanceMutex);
 | |
| 
 | |
|     uint32_t lockedImageCount = 0;
 | |
|     uint32_t totalSurfaceCount = 0;
 | |
|     uint32_t lockedSurfaceCount = 0;
 | |
|     for (const auto& cache : mImageCaches.Values()) {
 | |
|       totalSurfaceCount += cache->Count();
 | |
|       if (cache->IsLocked()) {
 | |
|         ++lockedImageCount;
 | |
|       }
 | |
|       for (const auto& value : cache->Values()) {
 | |
|         if (value->IsLocked()) {
 | |
|           ++lockedSurfaceCount;
 | |
|         }
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     // clang-format off
 | |
|     // We have explicit memory reporting for the surface cache which is more
 | |
|     // accurate than the cost metrics we report here, but these metrics are
 | |
|     // still useful to report, since they control the cache's behavior.
 | |
|     MOZ_COLLECT_REPORT(
 | |
|       "explicit/images/cache/overhead", KIND_HEAP, UNITS_BYTES,
 | |
|       ShallowSizeOfIncludingThis(SurfaceCacheMallocSizeOf, lock),
 | |
| "Memory used by the surface cache data structures, excluding surface data.");
 | |
| 
 | |
|     MOZ_COLLECT_REPORT(
 | |
|       "imagelib-surface-cache-estimated-total",
 | |
|       KIND_OTHER, UNITS_BYTES, (mMaxCost - mAvailableCost),
 | |
| "Estimated total memory used by the imagelib surface cache.");
 | |
| 
 | |
|     MOZ_COLLECT_REPORT(
 | |
|       "imagelib-surface-cache-estimated-locked",
 | |
|       KIND_OTHER, UNITS_BYTES, mLockedCost,
 | |
| "Estimated memory used by locked surfaces in the imagelib surface cache.");
 | |
| 
 | |
|     MOZ_COLLECT_REPORT(
 | |
|       "imagelib-surface-cache-tracked-cost-count",
 | |
|       KIND_OTHER, UNITS_COUNT, mCosts.Length(),
 | |
| "Total number of surfaces tracked for cost (and expiry) in the imagelib surface cache.");
 | |
| 
 | |
|     MOZ_COLLECT_REPORT(
 | |
|       "imagelib-surface-cache-tracked-expiry-count",
 | |
|       KIND_OTHER, UNITS_COUNT, mExpirationTracker.Length(lock),
 | |
| "Total number of surfaces tracked for expiry (and cost) in the imagelib surface cache.");
 | |
| 
 | |
|     MOZ_COLLECT_REPORT(
 | |
|       "imagelib-surface-cache-image-count",
 | |
|       KIND_OTHER, UNITS_COUNT, mImageCaches.Count(),
 | |
| "Total number of images in the imagelib surface cache.");
 | |
| 
 | |
|     MOZ_COLLECT_REPORT(
 | |
|       "imagelib-surface-cache-locked-image-count",
 | |
|       KIND_OTHER, UNITS_COUNT, lockedImageCount,
 | |
| "Total number of locked images in the imagelib surface cache.");
 | |
| 
 | |
|     MOZ_COLLECT_REPORT(
 | |
|       "imagelib-surface-cache-image-surface-count",
 | |
|       KIND_OTHER, UNITS_COUNT, totalSurfaceCount,
 | |
| "Total number of surfaces in the imagelib surface cache.");
 | |
| 
 | |
|     MOZ_COLLECT_REPORT(
 | |
|       "imagelib-surface-cache-locked-surfaces-count",
 | |
|       KIND_OTHER, UNITS_COUNT, lockedSurfaceCount,
 | |
| "Total number of locked surfaces in the imagelib surface cache.");
 | |
| 
 | |
|     MOZ_COLLECT_REPORT(
 | |
|       "imagelib-surface-cache-overflow-count",
 | |
|       KIND_OTHER, UNITS_COUNT, mOverflowCount,
 | |
| "Count of how many times the surface cache has hit its capacity and been "
 | |
| "unable to insert a new surface.");
 | |
| 
 | |
|     MOZ_COLLECT_REPORT(
 | |
|       "imagelib-surface-cache-tracking-failure-count",
 | |
|       KIND_OTHER, UNITS_COUNT, mTrackingFailureCount,
 | |
| "Count of how many times the surface cache has failed to begin tracking a "
 | |
| "given surface.");
 | |
| 
 | |
|     MOZ_COLLECT_REPORT(
 | |
|       "imagelib-surface-cache-already-present-count",
 | |
|       KIND_OTHER, UNITS_COUNT, mAlreadyPresentCount,
 | |
| "Count of how many times the surface cache has failed to insert a surface "
 | |
| "because it is already present.");
 | |
| 
 | |
|     MOZ_COLLECT_REPORT(
 | |
|       "imagelib-surface-cache-table-failure-count",
 | |
|       KIND_OTHER, UNITS_COUNT, mTableFailureCount,
 | |
| "Count of how many times the surface cache has failed to insert a surface "
 | |
| "because a hash table could not accept an entry.");
 | |
|     // clang-format on
 | |
| 
 | |
|     return NS_OK;
 | |
|   }
 | |
| 
 | |
|   void CollectSizeOfSurfaces(const ImageKey aImageKey,
 | |
|                              nsTArray<SurfaceMemoryCounter>& aCounters,
 | |
|                              MallocSizeOf aMallocSizeOf,
 | |
|                              const StaticMutexAutoLock& aAutoLock) {
 | |
|     RefPtr<ImageSurfaceCache> cache = GetImageCache(aImageKey);
 | |
|     if (!cache) {
 | |
|       return;  // No surfaces for this image.
 | |
|     }
 | |
| 
 | |
|     // Report all surfaces in the per-image cache.
 | |
|     cache->CollectSizeOfSurfaces(
 | |
|         aCounters, aMallocSizeOf,
 | |
|         [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);
 | |
|   }
 | |
| 
 | |
|   void ReleaseImageOnMainThread(already_AddRefed<image::Image>&& aImage,
 | |
|                                 const StaticMutexAutoLock& aAutoLock) {
 | |
|     RefPtr<image::Image> image = aImage;
 | |
|     if (!image) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     bool needsDispatch = mReleasingImagesOnMainThread.IsEmpty();
 | |
|     mReleasingImagesOnMainThread.AppendElement(image);
 | |
| 
 | |
|     if (!needsDispatch || gXPCOMThreadsShutDown) {
 | |
|       // Either there is already a ongoing task for ClearReleasingImages() or
 | |
|       // it's too late in shutdown to dispatch.
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     NS_DispatchToMainThread(NS_NewRunnableFunction(
 | |
|         "SurfaceCacheImpl::ReleaseImageOnMainThread",
 | |
|         []() -> void { SurfaceCache::ClearReleasingImages(); }));
 | |
|   }
 | |
| 
 | |
|   void TakeReleasingImages(nsTArray<RefPtr<image::Image>>& aImage,
 | |
|                            const StaticMutexAutoLock& aAutoLock) {
 | |
|     MOZ_ASSERT(NS_IsMainThread());
 | |
|     aImage.SwapElements(mReleasingImagesOnMainThread);
 | |
|   }
 | |
| 
 | |
|  private:
 | |
|   already_AddRefed<ImageSurfaceCache> GetImageCache(const ImageKey aImageKey) {
 | |
|     RefPtr<ImageSurfaceCache> imageCache;
 | |
|     mImageCaches.Get(aImageKey, getter_AddRefs(imageCache));
 | |
|     return imageCache.forget();
 | |
|   }
 | |
| 
 | |
|   void MaybeRemoveEmptyCache(const ImageKey aImageKey,
 | |
|                              ImageSurfaceCache* aCache) {
 | |
|     // Remove the per-image cache if it's unneeded now. Keep it if the image is
 | |
|     // locked, since the per-image cache is where we store that state. Note that
 | |
|     // we don't push it into mImageCachesDiscard because all of its surfaces
 | |
|     // have been removed, so it is safe to free while holding the lock.
 | |
|     if (aCache->IsEmpty() && !aCache->IsLocked()) {
 | |
|       mImageCaches.Remove(aImageKey);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   // This is similar to CanHold() except that it takes into account the costs of
 | |
|   // locked surfaces. It's used internally in Insert(), but it's not exposed
 | |
|   // publicly because we permit multithreaded access to the surface cache, which
 | |
|   // means that the result would be meaningless: another thread could insert a
 | |
|   // surface or lock an image at any time.
 | |
|   bool CanHoldAfterDiscarding(const Cost aCost) const {
 | |
|     return aCost <= mMaxCost - mLockedCost;
 | |
|   }
 | |
| 
 | |
|   bool MarkUsed(NotNull<CachedSurface*> aSurface,
 | |
|                 NotNull<ImageSurfaceCache*> aCache,
 | |
|                 const StaticMutexAutoLock& aAutoLock) {
 | |
|     if (aCache->IsLocked()) {
 | |
|       LockSurface(aSurface, aAutoLock);
 | |
|       return true;
 | |
|     }
 | |
| 
 | |
|     nsresult rv = mExpirationTracker.MarkUsedLocked(aSurface, aAutoLock);
 | |
|     if (NS_WARN_IF(NS_FAILED(rv))) {
 | |
|       // If mark used fails, it is because it failed to reinsert the surface
 | |
|       // after removing it from the tracker. Thus we need to update our
 | |
|       // own accounting but otherwise expect it to be untracked.
 | |
|       StopTracking(aSurface, /* aIsTracked */ false, aAutoLock);
 | |
|       return false;
 | |
|     }
 | |
|     return true;
 | |
|   }
 | |
| 
 | |
|   void DoUnlockSurfaces(NotNull<ImageSurfaceCache*> aCache, bool aStaticOnly,
 | |
|                         const StaticMutexAutoLock& aAutoLock) {
 | |
|     AutoTArray<NotNull<CachedSurface*>, 8> discard;
 | |
| 
 | |
|     // Unlock all the surfaces the per-image cache is holding.
 | |
|     for (const auto& value : aCache->Values()) {
 | |
|       NotNull<CachedSurface*> surface = WrapNotNull(value);
 | |
|       if (surface->IsPlaceholder() || !surface->IsLocked()) {
 | |
|         continue;
 | |
|       }
 | |
|       if (aStaticOnly &&
 | |
|           surface->GetSurfaceKey().Playback() != PlaybackType::eStatic) {
 | |
|         continue;
 | |
|       }
 | |
|       StopTracking(surface, /* aIsTracked */ true, aAutoLock);
 | |
|       surface->SetLocked(false);
 | |
|       if (MOZ_UNLIKELY(!StartTracking(surface, aAutoLock))) {
 | |
|         discard.AppendElement(surface);
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     // Discard any that we failed to track.
 | |
|     for (auto iter = discard.begin(); iter != discard.end(); ++iter) {
 | |
|       Remove(*iter, /* aStopTracking */ false, aAutoLock);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   void RemoveEntry(const ImageKey aImageKey, const SurfaceKey& aSurfaceKey,
 | |
|                    const StaticMutexAutoLock& aAutoLock) {
 | |
|     RefPtr<ImageSurfaceCache> cache = GetImageCache(aImageKey);
 | |
|     if (!cache) {
 | |
|       return;  // No cached surfaces for this image.
 | |
|     }
 | |
| 
 | |
|     RefPtr<CachedSurface> surface =
 | |
|         cache->Lookup(aSurfaceKey, /* aForAccess = */ false);
 | |
|     if (!surface) {
 | |
|       return;  // Lookup in the per-image cache missed.
 | |
|     }
 | |
| 
 | |
|     Remove(WrapNotNull(surface), /* aStopTracking */ true, aAutoLock);
 | |
|   }
 | |
| 
 | |
|   class SurfaceTracker final
 | |
|       : public ExpirationTrackerImpl<CachedSurface, 2, StaticMutex,
 | |
|                                      StaticMutexAutoLock> {
 | |
|    public:
 | |
|     explicit SurfaceTracker(uint32_t aSurfaceCacheExpirationTimeMS)
 | |
|         : ExpirationTrackerImpl<CachedSurface, 2, StaticMutex,
 | |
|                                 StaticMutexAutoLock>(
 | |
|               aSurfaceCacheExpirationTimeMS, "SurfaceTracker") {}
 | |
| 
 | |
|    protected:
 | |
|     void NotifyExpiredLocked(CachedSurface* aSurface,
 | |
|                              const StaticMutexAutoLock& aAutoLock) override {
 | |
|       sInstance->Remove(WrapNotNull(aSurface), /* aStopTracking */ true,
 | |
|                         aAutoLock);
 | |
|     }
 | |
| 
 | |
|     void NotifyHandlerEndLocked(const StaticMutexAutoLock& aAutoLock) override {
 | |
|       sInstance->TakeDiscard(mDiscard, aAutoLock);
 | |
|     }
 | |
| 
 | |
|     void NotifyHandlerEnd() override {
 | |
|       nsTArray<RefPtr<CachedSurface>> discard(std::move(mDiscard));
 | |
|     }
 | |
| 
 | |
|     StaticMutex& GetMutex() override { return sInstanceMutex; }
 | |
| 
 | |
|     nsTArray<RefPtr<CachedSurface>> mDiscard;
 | |
|   };
 | |
| 
 | |
|   class MemoryPressureObserver final : public nsIObserver {
 | |
|    public:
 | |
|     NS_DECL_ISUPPORTS
 | |
| 
 | |
|     NS_IMETHOD Observe(nsISupports*, const char* aTopic,
 | |
|                        const char16_t*) override {
 | |
|       nsTArray<RefPtr<CachedSurface>> discard;
 | |
|       {
 | |
|         StaticMutexAutoLock lock(sInstanceMutex);
 | |
|         if (sInstance && strcmp(aTopic, "memory-pressure") == 0) {
 | |
|           sInstance->DiscardForMemoryPressure(lock);
 | |
|           sInstance->TakeDiscard(discard, lock);
 | |
|         }
 | |
|       }
 | |
|       return NS_OK;
 | |
|     }
 | |
| 
 | |
|    private:
 | |
|     virtual ~MemoryPressureObserver() {}
 | |
|   };
 | |
| 
 | |
|   nsTArray<CostEntry> mCosts;
 | |
|   nsRefPtrHashtable<nsPtrHashKey<Image>, ImageSurfaceCache> mImageCaches;
 | |
|   nsTArray<RefPtr<CachedSurface>> mCachedSurfacesDiscard;
 | |
|   SurfaceTracker mExpirationTracker;
 | |
|   RefPtr<MemoryPressureObserver> mMemoryPressureObserver;
 | |
|   nsTArray<RefPtr<image::Image>> mReleasingImagesOnMainThread;
 | |
|   const uint32_t mDiscardFactor;
 | |
|   const Cost mMaxCost;
 | |
|   Cost mAvailableCost;
 | |
|   Cost mLockedCost;
 | |
|   size_t mOverflowCount;
 | |
|   size_t mAlreadyPresentCount;
 | |
|   size_t mTableFailureCount;
 | |
|   size_t mTrackingFailureCount;
 | |
| };
 | |
| 
 | |
| NS_IMPL_ISUPPORTS(SurfaceCacheImpl, nsIMemoryReporter)
 | |
| NS_IMPL_ISUPPORTS(SurfaceCacheImpl::MemoryPressureObserver, nsIObserver)
 | |
| 
 | |
| ///////////////////////////////////////////////////////////////////////////////
 | |
| // Public API
 | |
| ///////////////////////////////////////////////////////////////////////////////
 | |
| 
 | |
| /* static */
 | |
| void SurfaceCache::Initialize() {
 | |
|   // Initialize preferences.
 | |
|   MOZ_ASSERT(NS_IsMainThread());
 | |
|   MOZ_ASSERT(!sInstance, "Shouldn't initialize more than once");
 | |
| 
 | |
|   // See StaticPrefs for the default values of these preferences.
 | |
| 
 | |
|   // Length of time before an unused surface is removed from the cache, in
 | |
|   // milliseconds.
 | |
|   uint32_t surfaceCacheExpirationTimeMS =
 | |
|       StaticPrefs::image_mem_surfacecache_min_expiration_ms_AtStartup();
 | |
| 
 | |
|   // What fraction of the memory used by the surface cache we should discard
 | |
|   // when we get a memory pressure notification. This value is interpreted as
 | |
|   // 1/N, so 1 means to discard everything, 2 means to discard about half of the
 | |
|   // memory we're using, and so forth. We clamp it to avoid division by zero.
 | |
|   uint32_t surfaceCacheDiscardFactor =
 | |
|       max(StaticPrefs::image_mem_surfacecache_discard_factor_AtStartup(), 1u);
 | |
| 
 | |
|   // Maximum size of the surface cache, in kilobytes.
 | |
|   uint64_t surfaceCacheMaxSizeKB =
 | |
|       StaticPrefs::image_mem_surfacecache_max_size_kb_AtStartup();
 | |
| 
 | |
|   if (sizeof(uintptr_t) <= 4) {
 | |
|     // Limit surface cache to 1 GB if our address space is 32 bit.
 | |
|     surfaceCacheMaxSizeKB = 1024 * 1024;
 | |
|   }
 | |
| 
 | |
|   // A knob determining the actual size of the surface cache. Currently the
 | |
|   // cache is (size of main memory) / (surface cache size factor) KB
 | |
|   // or (surface cache max size) KB, whichever is smaller. The formula
 | |
|   // may change in the future, though.
 | |
|   // For example, a value of 4 would yield a 256MB cache on a 1GB machine.
 | |
|   // The smallest machines we are likely to run this code on have 256MB
 | |
|   // of memory, which would yield a 64MB cache on this setting.
 | |
|   // We clamp this value to avoid division by zero.
 | |
|   uint32_t surfaceCacheSizeFactor =
 | |
|       max(StaticPrefs::image_mem_surfacecache_size_factor_AtStartup(), 1u);
 | |
| 
 | |
|   // Compute the size of the surface cache.
 | |
|   uint64_t memorySize = PR_GetPhysicalMemorySize();
 | |
|   if (memorySize == 0) {
 | |
|     MOZ_ASSERT_UNREACHABLE("PR_GetPhysicalMemorySize not implemented here");
 | |
|     memorySize = 256 * 1024 * 1024;  // Fall back to 256MB.
 | |
|   }
 | |
|   uint64_t proposedSize = memorySize / surfaceCacheSizeFactor;
 | |
|   uint64_t surfaceCacheSizeBytes =
 | |
|       min(proposedSize, surfaceCacheMaxSizeKB * 1024);
 | |
|   uint32_t finalSurfaceCacheSizeBytes =
 | |
|       min(surfaceCacheSizeBytes, uint64_t(UINT32_MAX));
 | |
| 
 | |
|   // Create the surface cache singleton with the requested settings.  Note that
 | |
|   // the size is a limit that the cache may not grow beyond, but we do not
 | |
|   // actually allocate any storage for surfaces at this time.
 | |
|   sInstance = new SurfaceCacheImpl(surfaceCacheExpirationTimeMS,
 | |
|                                    surfaceCacheDiscardFactor,
 | |
|                                    finalSurfaceCacheSizeBytes);
 | |
|   sInstance->InitMemoryReporter();
 | |
| }
 | |
| 
 | |
| /* static */
 | |
| void SurfaceCache::Shutdown() {
 | |
|   RefPtr<SurfaceCacheImpl> cache;
 | |
|   {
 | |
|     StaticMutexAutoLock lock(sInstanceMutex);
 | |
|     MOZ_ASSERT(NS_IsMainThread());
 | |
|     MOZ_ASSERT(sInstance, "No singleton - was Shutdown() called twice?");
 | |
|     cache = sInstance.forget();
 | |
|   }
 | |
| }
 | |
| 
 | |
| /* static */
 | |
| LookupResult SurfaceCache::Lookup(const ImageKey aImageKey,
 | |
|                                   const SurfaceKey& aSurfaceKey,
 | |
|                                   bool aMarkUsed) {
 | |
|   nsTArray<RefPtr<CachedSurface>> discard;
 | |
|   LookupResult rv(MatchType::NOT_FOUND);
 | |
| 
 | |
|   {
 | |
|     StaticMutexAutoLock lock(sInstanceMutex);
 | |
|     if (!sInstance) {
 | |
|       return rv;
 | |
|     }
 | |
| 
 | |
|     rv = sInstance->Lookup(aImageKey, aSurfaceKey, lock, aMarkUsed);
 | |
|     sInstance->TakeDiscard(discard, lock);
 | |
|   }
 | |
| 
 | |
|   return rv;
 | |
| }
 | |
| 
 | |
| /* static */
 | |
| LookupResult SurfaceCache::LookupBestMatch(const ImageKey aImageKey,
 | |
|                                            const SurfaceKey& aSurfaceKey,
 | |
|                                            bool aMarkUsed) {
 | |
|   nsTArray<RefPtr<CachedSurface>> discard;
 | |
|   LookupResult rv(MatchType::NOT_FOUND);
 | |
| 
 | |
|   {
 | |
|     StaticMutexAutoLock lock(sInstanceMutex);
 | |
|     if (!sInstance) {
 | |
|       return rv;
 | |
|     }
 | |
| 
 | |
|     rv = sInstance->LookupBestMatch(aImageKey, aSurfaceKey, lock, aMarkUsed);
 | |
|     sInstance->TakeDiscard(discard, lock);
 | |
|   }
 | |
| 
 | |
|   return rv;
 | |
| }
 | |
| 
 | |
| /* static */
 | |
| InsertOutcome SurfaceCache::Insert(NotNull<ISurfaceProvider*> aProvider) {
 | |
|   nsTArray<RefPtr<CachedSurface>> discard;
 | |
|   InsertOutcome rv(InsertOutcome::FAILURE);
 | |
| 
 | |
|   {
 | |
|     StaticMutexAutoLock lock(sInstanceMutex);
 | |
|     if (!sInstance) {
 | |
|       return rv;
 | |
|     }
 | |
| 
 | |
|     rv = sInstance->Insert(aProvider, /* aSetAvailable = */ false, lock);
 | |
|     sInstance->TakeDiscard(discard, lock);
 | |
|   }
 | |
| 
 | |
|   return rv;
 | |
| }
 | |
| 
 | |
| /* static */
 | |
| bool SurfaceCache::CanHold(const IntSize& aSize,
 | |
|                            uint32_t aBytesPerPixel /* = 4 */) {
 | |
|   StaticMutexAutoLock lock(sInstanceMutex);
 | |
|   if (!sInstance) {
 | |
|     return false;
 | |
|   }
 | |
| 
 | |
|   Cost cost = ComputeCost(aSize, aBytesPerPixel);
 | |
|   return sInstance->CanHold(cost);
 | |
| }
 | |
| 
 | |
| /* static */
 | |
| bool SurfaceCache::CanHold(size_t aSize) {
 | |
|   StaticMutexAutoLock lock(sInstanceMutex);
 | |
|   if (!sInstance) {
 | |
|     return false;
 | |
|   }
 | |
| 
 | |
|   return sInstance->CanHold(aSize);
 | |
| }
 | |
| 
 | |
| /* static */
 | |
| void SurfaceCache::SurfaceAvailable(NotNull<ISurfaceProvider*> aProvider) {
 | |
|   StaticMutexAutoLock lock(sInstanceMutex);
 | |
|   if (!sInstance) {
 | |
|     return;
 | |
|   }
 | |
| 
 | |
|   sInstance->SurfaceAvailable(aProvider, lock);
 | |
| }
 | |
| 
 | |
| /* static */
 | |
| void SurfaceCache::LockImage(const ImageKey aImageKey) {
 | |
|   StaticMutexAutoLock lock(sInstanceMutex);
 | |
|   if (sInstance) {
 | |
|     return sInstance->LockImage(aImageKey);
 | |
|   }
 | |
| }
 | |
| 
 | |
| /* static */
 | |
| void SurfaceCache::UnlockImage(const ImageKey aImageKey) {
 | |
|   StaticMutexAutoLock lock(sInstanceMutex);
 | |
|   if (sInstance) {
 | |
|     return sInstance->UnlockImage(aImageKey, lock);
 | |
|   }
 | |
| }
 | |
| 
 | |
| /* static */
 | |
| void SurfaceCache::UnlockEntries(const ImageKey aImageKey) {
 | |
|   StaticMutexAutoLock lock(sInstanceMutex);
 | |
|   if (sInstance) {
 | |
|     return sInstance->UnlockEntries(aImageKey, lock);
 | |
|   }
 | |
| }
 | |
| 
 | |
| /* static */
 | |
| void SurfaceCache::RemoveImage(const ImageKey aImageKey) {
 | |
|   RefPtr<ImageSurfaceCache> discard;
 | |
|   {
 | |
|     StaticMutexAutoLock lock(sInstanceMutex);
 | |
|     if (sInstance) {
 | |
|       discard = sInstance->RemoveImage(aImageKey, lock);
 | |
|     }
 | |
|   }
 | |
| }
 | |
| 
 | |
| /* static */
 | |
| void SurfaceCache::PruneImage(const ImageKey aImageKey) {
 | |
|   nsTArray<RefPtr<CachedSurface>> discard;
 | |
|   {
 | |
|     StaticMutexAutoLock lock(sInstanceMutex);
 | |
|     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);
 | |
|     }
 | |
|   }
 | |
| }
 | |
| 
 | |
| /* static */
 | |
| void SurfaceCache::ResetAnimation(const ImageKey aImageKey,
 | |
|                                   const SurfaceKey& aSurfaceKey) {
 | |
|   RefPtr<CachedSurface> surface;
 | |
|   nsTArray<RefPtr<CachedSurface>> discard;
 | |
|   {
 | |
|     StaticMutexAutoLock lock(sInstanceMutex);
 | |
|     if (!sInstance) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     surface =
 | |
|         sInstance->GetSurfaceForResetAnimation(aImageKey, aSurfaceKey, lock);
 | |
|     sInstance->TakeDiscard(discard, lock);
 | |
|   }
 | |
| 
 | |
|   // Calling Reset will acquire the AnimationSurfaceProvider::mFramesMutex
 | |
|   // mutex. In other places we acquire the mFramesMutex then call into the
 | |
|   // surface cache (acquiring the surface cache mutex), so that determines a
 | |
|   // lock order which we must obey by calling Reset after releasing the surface
 | |
|   // cache mutex.
 | |
|   if (surface) {
 | |
|     DrawableSurface drawableSurface =
 | |
|         surface->GetDrawableSurfaceEvenIfPlaceholder();
 | |
|     if (drawableSurface) {
 | |
|       MOZ_ASSERT(surface->GetSurfaceKey() == aSurfaceKey,
 | |
|                  "ResetAnimation() not returning an exact match?");
 | |
| 
 | |
|       drawableSurface.Reset();
 | |
|     }
 | |
|   }
 | |
| }
 | |
| 
 | |
| /* static */
 | |
| void SurfaceCache::CollectSizeOfSurfaces(
 | |
|     const ImageKey aImageKey, nsTArray<SurfaceMemoryCounter>& aCounters,
 | |
|     MallocSizeOf aMallocSizeOf) {
 | |
|   nsTArray<RefPtr<CachedSurface>> discard;
 | |
|   {
 | |
|     StaticMutexAutoLock lock(sInstanceMutex);
 | |
|     if (!sInstance) {
 | |
|       return;
 | |
|     }
 | |
| 
 | |
|     sInstance->CollectSizeOfSurfaces(aImageKey, aCounters, aMallocSizeOf, lock);
 | |
|     sInstance->TakeDiscard(discard, lock);
 | |
|   }
 | |
| }
 | |
| 
 | |
| /* static */
 | |
| size_t SurfaceCache::MaximumCapacity() {
 | |
|   StaticMutexAutoLock lock(sInstanceMutex);
 | |
|   if (!sInstance) {
 | |
|     return 0;
 | |
|   }
 | |
| 
 | |
|   return sInstance->MaximumCapacity();
 | |
| }
 | |
| 
 | |
| /* static */
 | |
| bool SurfaceCache::IsLegalSize(const IntSize& aSize) {
 | |
|   // reject over-wide or over-tall images
 | |
|   const int32_t k64KLimit = 0x0000FFFF;
 | |
|   if (MOZ_UNLIKELY(aSize.width > k64KLimit || aSize.height > k64KLimit)) {
 | |
|     NS_WARNING("image too big");
 | |
|     return false;
 | |
|   }
 | |
| 
 | |
|   // protect against invalid sizes
 | |
|   if (MOZ_UNLIKELY(aSize.height <= 0 || aSize.width <= 0)) {
 | |
|     return false;
 | |
|   }
 | |
| 
 | |
|   // check to make sure we don't overflow a 32-bit
 | |
|   CheckedInt32 requiredBytes =
 | |
|       CheckedInt32(aSize.width) * CheckedInt32(aSize.height) * 4;
 | |
|   if (MOZ_UNLIKELY(!requiredBytes.isValid())) {
 | |
|     NS_WARNING("width or height too large");
 | |
|     return false;
 | |
|   }
 | |
|   return true;
 | |
| }
 | |
| 
 | |
| IntSize SurfaceCache::ClampVectorSize(const IntSize& aSize) {
 | |
|   // If we exceed the maximum, we need to scale the size downwards to fit.
 | |
|   // It shouldn't get here if it is significantly larger because
 | |
|   // VectorImage::UseSurfaceCacheForSize should prevent us from requesting
 | |
|   // a rasterized version of a surface greater than 4x the maximum.
 | |
|   int32_t maxSizeKB =
 | |
|       StaticPrefs::image_cache_max_rasterized_svg_threshold_kb();
 | |
|   if (maxSizeKB <= 0) {
 | |
|     return aSize;
 | |
|   }
 | |
| 
 | |
|   int64_t proposedKB = int64_t(aSize.width) * aSize.height / 256;
 | |
|   if (maxSizeKB >= proposedKB) {
 | |
|     return aSize;
 | |
|   }
 | |
| 
 | |
|   double scale = sqrt(double(maxSizeKB) / proposedKB);
 | |
|   return IntSize(int32_t(scale * aSize.width), int32_t(scale * aSize.height));
 | |
| }
 | |
| 
 | |
| IntSize SurfaceCache::ClampSize(ImageKey aImageKey, const IntSize& aSize) {
 | |
|   if (aImageKey->GetType() != imgIContainer::TYPE_VECTOR) {
 | |
|     return aSize;
 | |
|   }
 | |
| 
 | |
|   return ClampVectorSize(aSize);
 | |
| }
 | |
| 
 | |
| /* static */
 | |
| void SurfaceCache::ReleaseImageOnMainThread(
 | |
|     already_AddRefed<image::Image> aImage, bool aAlwaysProxy) {
 | |
|   if (NS_IsMainThread() && !aAlwaysProxy) {
 | |
|     RefPtr<image::Image> image = std::move(aImage);
 | |
|     return;
 | |
|   }
 | |
| 
 | |
|   // Don't try to dispatch the release after shutdown, we'll just leak the
 | |
|   // runnable.
 | |
|   if (gXPCOMThreadsShutDown) {
 | |
|     return;
 | |
|   }
 | |
| 
 | |
|   StaticMutexAutoLock lock(sInstanceMutex);
 | |
|   if (sInstance) {
 | |
|     sInstance->ReleaseImageOnMainThread(std::move(aImage), lock);
 | |
|   } else {
 | |
|     NS_ReleaseOnMainThread("SurfaceCache::ReleaseImageOnMainThread",
 | |
|                            std::move(aImage), /* aAlwaysProxy */ true);
 | |
|   }
 | |
| }
 | |
| 
 | |
| /* static */
 | |
| void SurfaceCache::ClearReleasingImages() {
 | |
|   MOZ_ASSERT(NS_IsMainThread());
 | |
| 
 | |
|   nsTArray<RefPtr<image::Image>> images;
 | |
|   {
 | |
|     StaticMutexAutoLock lock(sInstanceMutex);
 | |
|     if (sInstance) {
 | |
|       sInstance->TakeReleasingImages(images, lock);
 | |
|     }
 | |
|   }
 | |
| }
 | |
| 
 | |
| }  // namespace image
 | |
| }  // namespace mozilla
 |