Bug 1060869 (Part 1) - Add lifetime control to SurfaceCache. r=dholbert
☠☠ backed out by 94148e711456 ☠ ☠
authorSeth Fowler <seth@mozilla.com>
Wed, 26 Nov 2014 01:37:56 -0800
changeset 241914 73c52394b08b3f265140ab7217a639f33397bc4c
parent 241913 351d96ab947598e33700a46d25c47e7ca493f4d5
child 241915 0a67a77990229e8d51e0ff13ff47ee15f0730195
push id4311
push userraliiev@mozilla.com
push dateMon, 12 Jan 2015 19:37:41 +0000
treeherdermozilla-beta@150c9fed433b [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersdholbert
bugs1060869
milestone36.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1060869 (Part 1) - Add lifetime control to SurfaceCache. r=dholbert
image/src/RasterImage.cpp
image/src/SurfaceCache.cpp
image/src/SurfaceCache.h
image/src/VectorImage.cpp
--- a/image/src/RasterImage.cpp
+++ b/image/src/RasterImage.cpp
@@ -200,17 +200,18 @@ public:
 
     // Everything worked, so commit to these objects and mark ourselves ready.
     mDstRef = Move(tentativeDstRef);
     mState = eReady;
 
     // Insert the new surface into the cache immediately. We need to do this so
     // that we won't start multiple scaling jobs for the same size.
     SurfaceCache::Insert(mDstRef.get(), ImageKey(mImage.get()),
-                         RasterSurfaceKey(mDstSize.ToIntSize(), mImageFlags));
+                         RasterSurfaceKey(mDstSize.ToIntSize(), mImageFlags),
+                         Lifetime::Transient);
 
     return true;
   }
 
   NS_IMETHOD Run() MOZ_OVERRIDE
   {
     if (mState == eReady) {
       // Collect information from the frames that we need to scale.
@@ -254,19 +255,19 @@ public:
       // We're done, so release everything.
       mSrcRef.reset();
       mDstRef.reset();
     } else if (mState == eFinishWithError) {
       MOZ_ASSERT(NS_IsMainThread());
       NS_WARNING("HQ scaling failed");
 
       // Remove the frame from the cache since we know we don't need it.
-      SurfaceCache::RemoveIfPresent(ImageKey(mImage.get()),
-                                    RasterSurfaceKey(mDstSize.ToIntSize(),
-                                                     mImageFlags));
+      SurfaceCache::RemoveSurface(ImageKey(mImage.get()),
+                                  RasterSurfaceKey(mDstSize.ToIntSize(),
+                                                   mImageFlags));
 
       // Release everything we're holding, too.
       mSrcRef.reset();
       mDstRef.reset();
     } else {
       // mState must be eNew, which is invalid in Run().
       MOZ_ASSERT(false, "Need to call Init() before dispatching");
     }
@@ -359,17 +360,17 @@ RasterImage::~RasterImage()
     // Kill off our decode request, if it's pending.  (If not, this call is
     // harmless.)
     ReentrantMonitorAutoEnter lock(mDecodingMonitor);
     DecodePool::StopDecoding(this);
     mDecoder = nullptr;
   }
 
   // Release any HQ scaled frames from the surface cache.
-  SurfaceCache::Discard(this);
+  SurfaceCache::RemoveImage(ImageKey(this));
 
   mAnim = nullptr;
 
   // Total statistics
   num_containers--;
   total_source_bytes -= mSourceData.Length();
 
   if (NS_IsMainThread()) {
--- a/image/src/SurfaceCache.cpp
+++ b/image/src/SurfaceCache.cpp
@@ -113,69 +113,93 @@ private:
  * surface.
  */
 class CachedSurface
 {
   ~CachedSurface() {}
 public:
   NS_INLINE_DECL_REFCOUNTING(CachedSurface)
 
-  CachedSurface(imgFrame*         aSurface,
-                const IntSize     aTargetSize,
-                const Cost        aCost,
-                const ImageKey    aImageKey,
-                const SurfaceKey& aSurfaceKey)
+  CachedSurface(imgFrame*          aSurface,
+                const Cost         aCost,
+                const ImageKey     aImageKey,
+                const SurfaceKey&  aSurfaceKey,
+                const Lifetime     aLifetime)
     : mSurface(aSurface)
-    , mTargetSize(aTargetSize)
     , mCost(aCost)
     , mImageKey(aImageKey)
     , mSurfaceKey(aSurfaceKey)
+    , mLifetime(aLifetime)
   {
-    MOZ_ASSERT(mSurface, "Must have a valid SourceSurface");
+    MOZ_ASSERT(mSurface, "Must have a valid surface");
     MOZ_ASSERT(mImageKey, "Must have a valid image key");
   }
 
   DrawableFrameRef DrawableRef() const
   {
     return mSurface->DrawableRef();
   }
 
+  void SetLocked(bool aLocked)
+  {
+    if (aLocked && mLifetime == Lifetime::Persistent) {
+      // This may fail, and that's OK. We make no guarantees about whether
+      // locking is successful if you call SurfaceCache::LockImage() after
+      // SurfaceCache::Insert().
+      mDrawableRef = mSurface->DrawableRef();
+    } else {
+      mDrawableRef.reset();
+    }
+  }
+
+  bool IsLocked() const { return bool(mDrawableRef); }
+
   ImageKey GetImageKey() const { return mImageKey; }
   SurfaceKey GetSurfaceKey() const { return mSurfaceKey; }
   CostEntry GetCostEntry() { return image::CostEntry(this, mCost); }
   nsExpirationState* GetExpirationState() { return &mExpirationState; }
+  Lifetime GetLifetime() const { return mLifetime; }
 
 private:
   nsExpirationState  mExpirationState;
   nsRefPtr<imgFrame> mSurface;
-  const IntSize      mTargetSize;
+  DrawableFrameRef   mDrawableRef;
   const Cost         mCost;
   const ImageKey     mImageKey;
   const SurfaceKey   mSurfaceKey;
+  const Lifetime     mLifetime;
 };
 
 /*
  * 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.
  */
 class ImageSurfaceCache
 {
-  ~ImageSurfaceCache() {}
+  ~ImageSurfaceCache() { }
 public:
+  ImageSurfaceCache() : mLocked(false) { }
+
   NS_INLINE_DECL_REFCOUNTING(ImageSurfaceCache)
 
   typedef nsRefPtrHashtable<nsGenericHashKey<SurfaceKey>, CachedSurface> SurfaceTable;
 
   bool IsEmpty() const { return mSurfaces.Count() == 0; }
   
   void Insert(const SurfaceKey& aKey, CachedSurface* aSurface)
   {
     MOZ_ASSERT(aSurface, "Should have a surface");
+    MOZ_ASSERT(!mLocked || aSurface->GetLifetime() != Lifetime::Persistent ||
+               aSurface->IsLocked(),
+               "Inserting an unlocked persistent surface for a locked image");
     mSurfaces.Put(aKey, aSurface);
   }
 
   void Remove(CachedSurface* aSurface)
   {
     MOZ_ASSERT(aSurface, "Should have a surface");
     MOZ_ASSERT(mSurfaces.GetWeak(aSurface->GetSurfaceKey()),
         "Should not be removing a surface we don't have");
@@ -190,18 +214,22 @@ public:
     return surface.forget();
   }
 
   void ForEach(SurfaceTable::EnumReadFunction aFunction, void* aData)
   {
     mSurfaces.EnumerateRead(aFunction, aData);
   }
 
+  void SetLocked(bool aLocked) { mLocked = aLocked; }
+  bool IsLocked() const { return mLocked; }
+
 private:
   SurfaceTable mSurfaces;
+  bool         mLocked;
 };
 
 /*
  * 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.
@@ -213,16 +241,17 @@ public:
 
   SurfaceCacheImpl(uint32_t aSurfaceCacheExpirationTimeMS,
                    uint32_t aSurfaceCacheSize)
     : mExpirationTracker(MOZ_THIS_IN_INITIALIZER_LIST(),
                          aSurfaceCacheExpirationTimeMS)
     , mMemoryPressureObserver(new MemoryPressureObserver)
     , mMaxCost(aSurfaceCacheSize)
     , mAvailableCost(aSurfaceCacheSize)
+    , mLockedCost(0)
   {
     nsCOMPtr<nsIObserverService> os = services::GetObserverService();
     if (os)
       os->AddObserver(mMemoryPressureObserver, "memory-pressure", false);
   }
 
 private:
   virtual ~SurfaceCacheImpl()
@@ -234,91 +263,120 @@ private:
     UnregisterWeakMemoryReporter(this);
   }
 
 public:
   void InitMemoryReporter() {
     RegisterWeakMemoryReporter(this);
   }
 
-  void Insert(imgFrame*         aSurface,
-              IntSize           aTargetSize,
+  bool Insert(imgFrame*         aSurface,
               const Cost        aCost,
               const ImageKey    aImageKey,
-              const SurfaceKey& aSurfaceKey)
+              const SurfaceKey& aSurfaceKey,
+              Lifetime          aLifetime)
   {
     MOZ_ASSERT(!Lookup(aImageKey, aSurfaceKey),
                "Inserting a duplicate surface into the SurfaceCache");
 
-    // If this is bigger than the maximum cache size, refuse to cache it.
-    if (!CanHold(aCost))
-      return;
+    // If this is bigger than we can hold after discarding everything we can,
+    // refuse to cache it.
+    if (!CanHoldAfterDiscarding(aCost))
+      return false;
 
-    nsRefPtr<CachedSurface> surface =
-      new CachedSurface(aSurface, aTargetSize, aCost, aImageKey, aSurfaceKey);
-
-    // Remove elements in order of cost until we can fit this in the cache.
+    // 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 (aCost > mAvailableCost) {
       MOZ_ASSERT(!mCosts.IsEmpty(), "Removed everything and it still won't fit");
       Remove(mCosts.LastElement().GetSurface());
     }
 
     // Locate the appropriate per-image cache. If there's not an existing cache
     // for this image, create it.
     nsRefPtr<ImageSurfaceCache> cache = GetImageCache(aImageKey);
     if (!cache) {
       cache = new ImageSurfaceCache;
       mImageCaches.Put(aImageKey, cache);
     }
 
+    nsRefPtr<CachedSurface> surface =
+      new CachedSurface(aSurface, aCost, aImageKey, aSurfaceKey, aLifetime);
+
+    // We require that locking succeed if the image is locked and the surface is
+    // persistent; the caller may need to know this to handle errors correctly.
+    if (cache->IsLocked() && aLifetime == Lifetime::Persistent) {
+      surface->SetLocked(true);
+      if (!surface->IsLocked()) {
+        return false;
+      }
+    }
+
     // Insert.
     MOZ_ASSERT(aCost <= mAvailableCost, "Inserting despite too large a cost");
     cache->Insert(aSurfaceKey, surface);
     StartTracking(surface);
+
+    return true;
   }
 
   void Remove(CachedSurface* aSurface)
   {
     MOZ_ASSERT(aSurface, "Should have a surface");
     const ImageKey imageKey = aSurface->GetImageKey();
 
     nsRefPtr<ImageSurfaceCache> cache = GetImageCache(imageKey);
     MOZ_ASSERT(cache, "Shouldn't try to remove a surface with no image cache");
 
     StopTracking(aSurface);
     cache->Remove(aSurface);
 
-    // Remove the per-image cache if it's unneeded now.
-    if (cache->IsEmpty()) {
+    // 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.)
+    if (cache->IsEmpty() && !cache->IsLocked()) {
       mImageCaches.Remove(imageKey);
     }
   }
 
   void StartTracking(CachedSurface* aSurface)
   {
     CostEntry costEntry = aSurface->GetCostEntry();
     MOZ_ASSERT(costEntry.GetCost() <= mAvailableCost,
                "Cost too large and the caller didn't catch it");
 
     mAvailableCost -= costEntry.GetCost();
-    mCosts.InsertElementSorted(costEntry);
-    mExpirationTracker.AddObject(aSurface);
+
+    if (aSurface->IsLocked()) {
+      mLockedCost += costEntry.GetCost();
+      MOZ_ASSERT(mLockedCost <= mMaxCost, "Locked more than we can hold?");
+    } else {
+      mCosts.InsertElementSorted(costEntry);
+      mExpirationTracker.AddObject(aSurface);
+    }
   }
 
   void StopTracking(CachedSurface* aSurface)
   {
     MOZ_ASSERT(aSurface, "Should have a surface");
     CostEntry costEntry = aSurface->GetCostEntry();
 
-    mExpirationTracker.RemoveObject(aSurface);
-    DebugOnly<bool> foundInCosts = mCosts.RemoveElementSorted(costEntry);
+    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 {
+      mExpirationTracker.RemoveObject(aSurface);
+      DebugOnly<bool> foundInCosts = mCosts.RemoveElementSorted(costEntry);
+      MOZ_ASSERT(foundInCosts, "Lost track of costs for this surface");
+    }
+
     mAvailableCost += costEntry.GetCost();
-
-    MOZ_ASSERT(foundInCosts, "Lost track of costs for this surface");
-    MOZ_ASSERT(mAvailableCost <= mMaxCost, "More available cost than we started with");
+    MOZ_ASSERT(mAvailableCost <= mMaxCost,
+               "More available cost than we started with");
   }
 
   DrawableFrameRef Lookup(const ImageKey    aImageKey,
                           const SurfaceKey& aSurfaceKey)
   {
     nsRefPtr<ImageSurfaceCache> cache = GetImageCache(aImageKey);
     if (!cache)
       return DrawableFrameRef();  // No cached surfaces for this image.
@@ -330,22 +388,25 @@ public:
     DrawableFrameRef ref = surface->DrawableRef();
     if (!ref) {
       // The surface was released by the operating system. Remove the cache
       // entry as well.
       Remove(surface);
       return DrawableFrameRef();
     }
 
-    mExpirationTracker.MarkUsed(surface);
+    if (!surface->IsLocked()) {
+      mExpirationTracker.MarkUsed(surface);
+    }
+
     return ref;
   }
 
-  void RemoveIfPresent(const ImageKey    aImageKey,
-                       const SurfaceKey& aSurfaceKey)
+  void RemoveSurface(const ImageKey    aImageKey,
+                     const SurfaceKey& aSurfaceKey)
   {
     nsRefPtr<ImageSurfaceCache> cache = GetImageCache(aImageKey);
     if (!cache)
       return;  // No cached surfaces for this image.
 
     nsRefPtr<CachedSurface> surface = cache->Lookup(aSurfaceKey);
     if (!surface)
       return;  // Lookup in the per-image cache missed.
@@ -353,77 +414,171 @@ public:
     Remove(surface);
   }
 
   bool CanHold(const Cost aCost) const
   {
     return aCost <= mMaxCost;
   }
 
-  void Discard(const ImageKey aImageKey)
+  void LockImage(const ImageKey aImageKey)
+  {
+    nsRefPtr<ImageSurfaceCache> cache = GetImageCache(aImageKey);
+    if (!cache) {
+      cache = new ImageSurfaceCache;
+      mImageCaches.Put(aImageKey, cache);
+    }
+
+    cache->SetLocked(true);
+
+    // Try to lock all the surfaces the per-image cache is holding.
+    cache->ForEach(DoLockSurface, this);
+  }
+
+  void UnlockImage(const ImageKey aImageKey)
+  {
+    nsRefPtr<ImageSurfaceCache> cache = GetImageCache(aImageKey);
+    if (!cache)
+      return;  // Already unlocked and removed.
+
+    cache->SetLocked(false);
+
+    // Unlock all the surfaces the per-image cache is holding.
+    cache->ForEach(DoUnlockSurface, this);
+  }
+
+  void RemoveImage(const ImageKey aImageKey)
   {
     nsRefPtr<ImageSurfaceCache> cache = GetImageCache(aImageKey);
     if (!cache)
       return;  // 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.
     cache->ForEach(DoStopTracking, this);
 
     // 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);
   }
 
   void DiscardAll()
   {
     // Remove in order of cost because mCosts is an array and the other data
-    // structures are all hash tables.
+    // structures are all hash tables. Note that locked surfaces (persistent
+    // surfaces belonging to locked images) are not removed, since they aren't
+    // present in mCosts.
     while (!mCosts.IsEmpty()) {
       Remove(mCosts.LastElement().GetSurface());
     }
   }
 
   static PLDHashOperator DoStopTracking(const SurfaceKey&,
                                         CachedSurface*    aSurface,
                                         void*             aCache)
   {
     static_cast<SurfaceCacheImpl*>(aCache)->StopTracking(aSurface);
     return PL_DHASH_NEXT;
   }
 
-  NS_IMETHOD
-  CollectReports(nsIHandleReportCallback* aHandleReport, nsISupports* aData,
-                 bool aAnonymize)
+  static PLDHashOperator DoLockSurface(const SurfaceKey&,
+                                       CachedSurface*    aSurface,
+                                       void*             aCache)
+  {
+    if (aSurface->GetLifetime() == Lifetime::Transient ||
+        aSurface->IsLocked()) {
+      return PL_DHASH_NEXT;
+    }
+
+    auto cache = static_cast<SurfaceCacheImpl*>(aCache);
+    cache->StopTracking(aSurface);
+
+    // Lock the surface. This can fail.
+    aSurface->SetLocked(true);
+    cache->StartTracking(aSurface);
+
+    return PL_DHASH_NEXT;
+  }
+
+  static PLDHashOperator DoUnlockSurface(const SurfaceKey&,
+                                         CachedSurface*    aSurface,
+                                         void*             aCache)
   {
-    return MOZ_COLLECT_REPORT(
-      "imagelib-surface-cache", KIND_OTHER, UNITS_BYTES,
-      SizeOfSurfacesEstimate(),
-      "Memory used by the imagelib temporary surface cache.");
+    if (aSurface->GetLifetime() == Lifetime::Transient ||
+        !aSurface->IsLocked()) {
+      return PL_DHASH_NEXT;
+    }
+
+    auto cache = static_cast<SurfaceCacheImpl*>(aCache);
+    cache->StopTracking(aSurface);
+
+    aSurface->SetLocked(false);
+    cache->StartTracking(aSurface);
+
+    return PL_DHASH_NEXT;
+  }
+
+  NS_IMETHOD
+  CollectReports(nsIHandleReportCallback* aHandleReport,
+                 nsISupports*             aData,
+                 bool                     aAnonymize)
+  {
+    nsresult rv;
+
+    rv = MOZ_COLLECT_REPORT("imagelib-surface-cache-total",
+                            KIND_OTHER, UNITS_BYTES,
+                            SizeOfSurfacesEstimate(),
+                            "Total memory used by the imagelib surface cache.");
+    NS_ENSURE_SUCCESS(rv, rv);
+
+    rv = MOZ_COLLECT_REPORT("imagelib-surface-cache-locked",
+                            KIND_OTHER, UNITS_BYTES,
+                            SizeOfLockedSurfacesEstimate(),
+                            "Memory used by locked surfaces in the imagelib "
+                            "surface cache.");
+    NS_ENSURE_SUCCESS(rv, rv);
+
+    return NS_OK;
   }
 
   // XXX(seth): This is currently only an estimate and, since we don't know
   // which surfaces are in GPU memory and which aren't, it's reported as
   // KIND_OTHER and will also show up in heap-unclassified. Bug 923302 will
   // make this nicer.
   Cost SizeOfSurfacesEstimate() const
   {
     return mMaxCost - mAvailableCost;
   }
 
+  Cost SizeOfLockedSurfacesEstimate() const
+  {
+    return mLockedCost;
+  }
+
 private:
   already_AddRefed<ImageSurfaceCache> GetImageCache(const ImageKey aImageKey)
   {
     nsRefPtr<ImageSurfaceCache> imageCache;
     mImageCaches.Get(aImageKey, getter_AddRefs(imageCache));
     return imageCache.forget();
   }
 
+  // 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 if we start permitting multithreaded access to the surface
+  // cache, which seems likely, then the result would be meaningless: another
+  // thread could insert a persistent surface or lock an image at any time.
+  bool CanHoldAfterDiscarding(const Cost aCost) const
+  {
+    return aCost <= mMaxCost - mLockedCost;
+  }
+
   struct SurfaceTracker : public nsExpirationTracker<CachedSurface, 2>
   {
     SurfaceTracker(SurfaceCacheImpl* aCache, uint32_t aSurfaceCacheExpirationTimeMS)
       : nsExpirationTracker<CachedSurface, 2>(aSurfaceCacheExpirationTimeMS)
       , mCache(aCache)
     { }
 
   protected:
@@ -456,60 +611,65 @@ private:
 
 
   nsTArray<CostEntry>                                       mCosts;
   nsRefPtrHashtable<nsPtrHashKey<Image>, ImageSurfaceCache> mImageCaches;
   SurfaceTracker                                            mExpirationTracker;
   nsRefPtr<MemoryPressureObserver>                          mMemoryPressureObserver;
   const Cost                                                mMaxCost;
   Cost                                                      mAvailableCost;
+  Cost                                                      mLockedCost;
 };
 
 NS_IMPL_ISUPPORTS(SurfaceCacheImpl, nsIMemoryReporter)
 NS_IMPL_ISUPPORTS(SurfaceCacheImpl::MemoryPressureObserver, nsIObserver)
 
 ///////////////////////////////////////////////////////////////////////////////
 // Public API
 ///////////////////////////////////////////////////////////////////////////////
 
 /* static */ void
 SurfaceCache::Initialize()
 {
   // Initialize preferences.
   MOZ_ASSERT(!sInstance, "Shouldn't initialize more than once");
 
-  // See gfxPrefs for the default values
+  // See gfxPrefs for the default values of these preferences.
 
-  // Length of time before an unused surface is removed from the cache, in milliseconds.
-  uint32_t surfaceCacheExpirationTimeMS = gfxPrefs::ImageMemSurfaceCacheMinExpirationMS();
+  // Length of time before an unused surface is removed from the cache, in
+  // milliseconds.
+  uint32_t surfaceCacheExpirationTimeMS =
+    gfxPrefs::ImageMemSurfaceCacheMinExpirationMS();
 
   // Maximum size of the surface cache, in kilobytes.
-  uint32_t surfaceCacheMaxSizeKB = gfxPrefs::ImageMemSurfaceCacheMaxSizeKB();
+  uint64_t surfaceCacheMaxSizeKB = gfxPrefs::ImageMemSurfaceCacheMaxSizeKB();
 
   // 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 64 would yield a 64MB cache on a 4GB machine.
+  // 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 4MB cache on the default setting.
+  // of memory, which would yield a 64MB cache on this setting.
   uint32_t surfaceCacheSizeFactor = gfxPrefs::ImageMemSurfaceCacheSizeFactor();
 
   // Clamp to avoid division by zero below.
   surfaceCacheSizeFactor = max(surfaceCacheSizeFactor, 1u);
 
   // Compute the size of the surface cache.
-  uint32_t proposedSize = PR_GetPhysicalMemorySize() / surfaceCacheSizeFactor;
-  uint32_t surfaceCacheSizeBytes = min(proposedSize, surfaceCacheMaxSizeKB * 1024);
+  uint64_t proposedSize = PR_GetPhysicalMemorySize() / 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 expiration time and
   // size. 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,
-                                   surfaceCacheSizeBytes);
+                                   finalSurfaceCacheSizeBytes);
   sInstance->InitMemoryReporter();
 }
 
 /* static */ void
 SurfaceCache::Shutdown()
 {
   MOZ_ASSERT(sInstance, "No singleton - was Shutdown() called twice?");
   sInstance = nullptr;
@@ -522,57 +682,77 @@ SurfaceCache::Lookup(const ImageKey    a
   MOZ_ASSERT(NS_IsMainThread());
   if (!sInstance) {
     return DrawableFrameRef();
   }
 
   return sInstance->Lookup(aImageKey, aSurfaceKey);
 }
 
-/* static */ void
+/* static */ bool
 SurfaceCache::Insert(imgFrame*         aSurface,
                      const ImageKey    aImageKey,
-                     const SurfaceKey& aSurfaceKey)
+                     const SurfaceKey& aSurfaceKey,
+                     Lifetime          aLifetime)
 {
   MOZ_ASSERT(NS_IsMainThread());
-  if (sInstance) {
-    Cost cost = ComputeCost(aSurfaceKey.Size());
-    sInstance->Insert(aSurface, aSurfaceKey.Size(), cost, aImageKey,
-                      aSurfaceKey);
+  if (!sInstance) {
+    return false;
   }
+
+  Cost cost = ComputeCost(aSurfaceKey.Size());
+  return sInstance->Insert(aSurface, cost, aImageKey, aSurfaceKey, aLifetime);
 }
 
 /* static */ bool
 SurfaceCache::CanHold(const IntSize& aSize)
 {
   MOZ_ASSERT(NS_IsMainThread());
   if (!sInstance) {
     return false;
   }
 
   Cost cost = ComputeCost(aSize);
   return sInstance->CanHold(cost);
 }
 
 /* static */ void
-SurfaceCache::RemoveIfPresent(const ImageKey    aImageKey,
-                              const SurfaceKey& aSurfaceKey)
+SurfaceCache::LockImage(Image* aImageKey)
 {
   MOZ_ASSERT(NS_IsMainThread());
   if (sInstance) {
-    sInstance->RemoveIfPresent(aImageKey, aSurfaceKey);
+    return sInstance->LockImage(aImageKey);
   }
 }
 
 /* static */ void
-SurfaceCache::Discard(Image* aImageKey)
+SurfaceCache::UnlockImage(Image* aImageKey)
 {
   MOZ_ASSERT(NS_IsMainThread());
   if (sInstance) {
-    sInstance->Discard(aImageKey);
+    return sInstance->UnlockImage(aImageKey);
+  }
+}
+
+/* static */ void
+SurfaceCache::RemoveSurface(const ImageKey    aImageKey,
+                            const SurfaceKey& aSurfaceKey)
+{
+  MOZ_ASSERT(NS_IsMainThread());
+  if (sInstance) {
+    sInstance->RemoveSurface(aImageKey, aSurfaceKey);
+  }
+}
+
+/* static */ void
+SurfaceCache::RemoveImage(Image* aImageKey)
+{
+  MOZ_ASSERT(NS_IsMainThread());
+  if (sInstance) {
+    sInstance->RemoveImage(aImageKey);
   }
 }
 
 /* static */ void
 SurfaceCache::DiscardAll()
 {
   MOZ_ASSERT(NS_IsMainThread());
   if (sInstance) {
--- a/image/src/SurfaceCache.h
+++ b/image/src/SurfaceCache.h
@@ -104,114 +104,197 @@ VectorSurfaceKey(const gfx::IntSize& aSi
                  const float aAnimationTime)
 {
   // We don't care about aFlags for VectorImage because none of the flags we
   // have right now influence VectorImage's rendering. If we add a new flag that
   // *does* affect how a VectorImage renders, we'll have to change this.
   return SurfaceKey(aSize, aSVGContext, aAnimationTime, 0);
 }
 
+MOZ_BEGIN_ENUM_CLASS(Lifetime, uint8_t)
+  Transient,
+  Persistent
+MOZ_END_ENUM_CLASS(Lifetime)
+
 /**
  * SurfaceCache is an imagelib-global service that allows caching of temporary
- * surfaces. Surfaces expire from the cache automatically if they go too long
- * without being accessed.
+ * surfaces. Surfaces normally expire from the cache automatically if they go
+ * too long without being accessed.
  *
  * SurfaceCache does not hold surfaces directly; instead, it holds imgFrame
  * objects, which hold surfaces but also layer on additional features specific
  * to imagelib's needs like animation, padding support, and transparent support
  * for volatile buffers.
  *
+ * Sometime it's useful to temporarily prevent surfaces from expiring from the
+ * cache. This is most often because losing the data could harm the user
+ * experience (for example, we often don't want to allow surfaces that are
+ * currently visible to expire) or because it's not possible to rematerialize
+ * the surface. SurfaceCache supports this through the use of image locking and
+ * surface lifetimes; see the comments for Insert() and LockImage() for more
+ * details.
+ *
+ * Any image which stores surfaces in the SurfaceCache *must* ensure that it
+ * calls RemoveImage() before it is destroyed. See the comments for
+ * RemoveImage() for more details.
+ *
  * SurfaceCache is not thread-safe; it should only be accessed from the main
  * thread.
  */
 struct SurfaceCache
 {
   typedef gfx::IntSize IntSize;
 
-  /*
+  /**
    * Initialize static data. Called during imagelib module initialization.
    */
   static void Initialize();
 
-  /*
+  /**
    * Release static data. Called during imagelib module shutdown.
    */
   static void Shutdown();
 
-  /*
+  /**
    * Look up the imgFrame containing a surface in the cache and returns a
    * drawable reference to that imgFrame.
    *
    * If the imgFrame was found in the cache, but had stored its surface in a
    * volatile buffer which was discarded by the OS, then it is automatically
-   * removed from the cache and an empty DrawableFrameRef is returned.
+   * removed from the cache and an empty DrawableFrameRef is returned. Note that
+   * this will never happen to persistent surfaces associated with a locked
+   * image; the cache keeps a strong reference to such surfaces internally.
    *
    * @param aImageKey    Key data identifying which image the surface belongs to.
    * @param aSurfaceKey  Key data which uniquely identifies the requested surface.
    *
    * @return a DrawableFrameRef to the imgFrame wrapping the requested surface,
    *         or an empty DrawableFrameRef if not found.
    */
   static DrawableFrameRef Lookup(const ImageKey    aImageKey,
                                  const SurfaceKey& aSurfaceKey);
 
-  /*
+  /**
    * Insert a surface into the cache. It is an error to call this function
    * without first calling Lookup to verify that the surface is not already in
    * the cache.
    *
+   * Each surface in the cache has a lifetime, either Transient or Persistent.
+   * Transient surfaces can expire from the cache at any time. Persistent
+   * surfaces can ordinarily also expire from the cache at any time, but if the
+   * image they're associated with is locked, then these surfaces will never
+   * expire. This means that surfaces which cannot be rematerialized should be
+   * inserted with a persistent lifetime *after* the image is locked with
+   * LockImage(); if you use the other order, the surfaces might expire before
+   * LockImage() gets called.
+   *
+   * If a surface cannot be rematerialized, it may be important to know whether
+   * it was inserted into the cache successfully. Insert() returns false if it
+   * failed to insert the surface, which could happen because of capacity
+   * reasons, or because it was already freed by the OS. If you aren't inserting
+   * a surface with persistent lifetime, or if the surface isn't associated with
+   * a locked image, the return value is useless: the surface might expire
+   * immediately after being inserted, even though Insert() returned true. Thus,
+   * most callers do not need to check the return value.
+   *
    * @param aTarget      The new surface (wrapped in an imgFrame) to insert into
    *                     the cache.
    * @param aImageKey    Key data identifying which image the surface belongs to.
    * @param aSurfaceKey  Key data which uniquely identifies the requested surface.
+   * @param aLifetime    Whether this is a transient surface that can always be
+   *                     allowed to expire, or a persistent surface that
+   *                     shouldn't expire if the image is locked.
+   * @return false if the surface could not be inserted. Only check this if
+   *         inserting a persistent surface associated with a locked image (see
+   *         above for more information).
    */
-  static void Insert(imgFrame*         aSurface,
+  static bool Insert(imgFrame*         aSurface,
                      const ImageKey    aImageKey,
-                     const SurfaceKey& aSurfaceKey);
+                     const SurfaceKey& aSurfaceKey,
+                     Lifetime          aLifetime);
 
-  /*
+  /**
    * Checks if a surface of a given size could possibly be stored in the cache.
    * If CanHold() returns false, Insert() will always fail to insert the
    * surface, but the inverse is not true: Insert() may take more information
    * into account than just image size when deciding whether to cache the
    * surface, so Insert() may still fail even if CanHold() returns true.
    *
    * Use CanHold() to avoid the need to create a temporary surface when we know
    * for sure the cache can't hold it.
    *
    * @param aSize  The dimensions of a surface in pixels.
    *
    * @return false if the surface cache can't hold a surface of that size.
    */
   static bool CanHold(const IntSize& aSize);
 
-  /*
-   * Removes a surface from the cache, if it's present.
+  /**
+   * Locks an image, preventing any of that image's surfaces from expiring
+   * unless they have a transient lifetime.
+   *
+   * Regardless of locking, any of an image's surfaces may be removed using
+   * RemoveSurface(), and all of an image's surfaces are removed by
+   * RemoveImage(), whether the image is locked or not.
+   *
+   * It's safe to call LockImage() on an image that's already locked; this has
+   * no effect.
+   *
+   * You must always unlock any image you lock. You may do this explicitly by
+   * calling UnlockImage(), or implicitly by calling RemoveImage(). Since you're
+   * required to call RemoveImage() when you destroy an image, this doesn't
+   * impose any additional requirements, but it's preferable to call
+   * UnlockImage() earlier if it's possible.
+   *
+   * @param aImageKey    The image to lock.
+   */
+  static void LockImage(const ImageKey aImageKey);
+
+  /**
+   * Unlocks an image, allowing any of its surfaces to expire at any time.
+   *
+   * It's OK to call UnlockImage() on an image that's already unlocked; this has
+   * no effect.
+   *
+   * @param aImageKey    The image to lock.
+   */
+  static void UnlockImage(const ImageKey aImageKey);
+
+  /**
+   * Removes a surface from the cache, if it's present. If it's not present,
+   * RemoveSurface() has no effect.
    *
    * Use this function to remove individual surfaces that have become invalid.
-   * Prefer Discard() or DiscardAll() when they're applicable, as they have much
-   * better performance than calling this function repeatedly.
+   * Prefer RemoveImage() or DiscardAll() when they're applicable, as they have
+   * much better performance than calling this function repeatedly.
    *
    * @param aImageKey    Key data identifying which image the surface belongs to.
    * @param aSurfaceKey  Key data which uniquely identifies the requested surface.
    */
-  static void RemoveIfPresent(const ImageKey    aImageKey,
-                              const SurfaceKey& aSurfaceKey);
-  /*
-   * Evicts any cached surfaces associated with the given image from the cache.
-   * This MUST be called, at a minimum, when the image is destroyed. If
-   * another image were allocated at the same address it could result in
-   * subtle, difficult-to-reproduce bugs.
+  static void RemoveSurface(const ImageKey    aImageKey,
+                            const SurfaceKey& aSurfaceKey);
+
+  /**
+   * Removes all cached surfaces associated with the given image from the cache.
+   * If the image is locked, it is automatically unlocked.
+   *
+   * This MUST be called, at a minimum, when an Image which could be storing
+   * surfaces 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 Discard(const ImageKey aImageKey);
+  static void RemoveImage(const ImageKey aImageKey);
 
-  /*
-   * Evicts all caches surfaces from ths cache.
+  /**
+   * Evicts all evictable surfaces from the cache.
+   *
+   * All surfaces are evictable except for persistent surfaces associated with
+   * locked images. Non-evictable surfaces can only be removed by
+   * RemoveSurface() or RemoveImage().
    */
   static void DiscardAll();
 
 private:
   virtual ~SurfaceCache() = 0;  // Forbid instantiation.
 };
 
 } // namespace image
--- a/image/src/VectorImage.cpp
+++ b/image/src/VectorImage.cpp
@@ -332,17 +332,17 @@ VectorImage::VectorImage(ProgressTracker
   mHasPendingInvalidation(false)
 {
   mProgressTrackerInit = new ProgressTrackerInit(this, aProgressTracker);
 }
 
 VectorImage::~VectorImage()
 {
   CancelAllListeners();
-  SurfaceCache::Discard(this);
+  SurfaceCache::RemoveImage(ImageKey(this));
 }
 
 //------------------------------------------------------------------------------
 // Methods inherited from Image.h
 
 nsresult
 VectorImage::Init(const char* aMimeType,
                   uint32_t aFlags)
@@ -560,17 +560,17 @@ VectorImage::SendInvalidationNotificatio
   // InvalidateObserversOnNextRefreshDriverTick, because RequestRefresh is never
   // called for them. Ordinarily this isn't needed, since we send out
   // invalidation notifications in OnSVGDocumentLoaded, but in rare cases the
   // SVG document may not be 100% ready to render at that time. In those cases
   // we would miss the subsequent invalidations if we didn't send out the
   // notifications directly in |InvalidateObservers...|.
 
   if (mProgressTracker) {
-    SurfaceCache::Discard(this);
+    SurfaceCache::RemoveImage(ImageKey(this));
     mProgressTracker->SyncNotifyProgress(FLAG_FRAME_COMPLETE,
                                          nsIntRect::GetMaxSizedIntRect());
   }
 }
 
 NS_IMETHODIMP_(nsIntRect)
 VectorImage::GetImageSpaceInvalidationRect(const nsIntRect& aRect)
 {
@@ -898,17 +898,18 @@ VectorImage::CreateSurfaceAndShow(const 
   RefPtr<SourceSurface> surface = frame->GetSurface();
   if (!surface)
     return Show(svgDrawable, aParams);
 
   // Attempt to cache the frame.
   SurfaceCache::Insert(frame, ImageKey(this),
                        VectorSurfaceKey(aParams.size,
                                         aParams.svgContext,
-                                        aParams.animationTime));
+                                        aParams.animationTime),
+                       Lifetime::Transient);
 
   // Draw.
   nsRefPtr<gfxDrawable> drawable =
     new gfxSurfaceDrawable(surface, ThebesIntSize(aParams.size));
   Show(drawable, aParams);
 }
 
 
@@ -966,17 +967,17 @@ VectorImage::UnlockImage()
   return NS_OK;
 }
 
 //******************************************************************************
 /* void requestDiscard() */
 NS_IMETHODIMP
 VectorImage::RequestDiscard()
 {
-  SurfaceCache::Discard(this);
+  SurfaceCache::RemoveImage(ImageKey(this));
   return NS_OK;
 }
 
 //******************************************************************************
 /* void resetAnimation (); */
 NS_IMETHODIMP
 VectorImage::ResetAnimation()
 {