Bug 764299 (Part 2) - Add a temporary surface cache to imagelib. r=dholbert
authorSeth Fowler <seth@mozilla.com>
Mon, 21 Oct 2013 18:10:43 +0200
changeset 166264 4d8068fd3271c0ad2eab42037e41702deb559598
parent 166263 bc31e05eddb8d0e161dc0a11112cc2232e813aeb
child 166265 5bedd82346b44f3072734f05612d1e8dcecdb16a
push id428
push userbbajaj@mozilla.com
push dateTue, 28 Jan 2014 00:16:25 +0000
treeherdermozilla-release@cd72a7ff3a75 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersdholbert
bugs764299
milestone27.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 764299 (Part 2) - Add a temporary surface cache to imagelib. r=dholbert
content/svg/content/src/moz.build
image/build/nsImageModule.cpp
image/src/SurfaceCache.cpp
image/src/SurfaceCache.h
image/src/moz.build
modules/libpref/src/init/all.js
--- a/content/svg/content/src/moz.build
+++ b/content/svg/content/src/moz.build
@@ -3,16 +3,17 @@
 # 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/.
 
 MODULE = 'content'
 
 EXPORTS += [
     'SVGAttrValueWrapper.h',
+    'SVGPreserveAspectRatio.h',
     'SVGStringList.h',
     'nsSVGClass.h',
     'nsSVGElement.h',
     'nsSVGFeatures.h',
 ]
 
 EXPORTS.mozilla.dom += [
     'SVGAElement.h',
--- a/image/build/nsImageModule.cpp
+++ b/image/build/nsImageModule.cpp
@@ -4,16 +4,17 @@
  * 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/. */
 
 #include "mozilla/ModuleUtils.h"
 #include "nsMimeTypes.h"
 
 #include "ImageFactory.h"
 #include "RasterImage.h"
+#include "SurfaceCache.h"
 
 #include "imgLoader.h"
 #include "imgRequest.h"
 #include "imgRequestProxy.h"
 #include "imgTools.h"
 #include "DiscardTracker.h"
 
 #include "nsICOEncoder.h"
@@ -79,24 +80,26 @@ static const mozilla::Module::CategoryEn
 };
 
 static nsresult
 imglib_Initialize()
 {
   mozilla::image::DiscardTracker::Initialize();
   mozilla::image::ImageFactory::Initialize();
   mozilla::image::RasterImage::Initialize();
+  mozilla::image::SurfaceCache::Initialize();
   imgLoader::GlobalInit();
   return NS_OK;
 }
 
 static void
 imglib_Shutdown()
 {
   imgLoader::Shutdown();
+  mozilla::image::SurfaceCache::Shutdown();
   mozilla::image::DiscardTracker::Shutdown();
 }
 
 static const mozilla::Module kImageModule = {
   mozilla::Module::kVersion,
   kImageCIDs,
   kImageContracts,
   kImageCategories,
new file mode 100644
--- /dev/null
+++ b/image/src/SurfaceCache.cpp
@@ -0,0 +1,490 @@
+/* -*- 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 "mozilla/Attributes.h"  // for MOZ_THIS_IN_INITIALIZER_LIST
+#include "mozilla/DebugOnly.h"
+#include "mozilla/Preferences.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/Util.h"  // for Maybe
+#include "gfxASurface.h"
+#include "gfxPattern.h"  // Workaround for flaw in bug 921753 part 2.
+#include "gfxDrawable.h"
+#include "gfxPlatform.h"
+#include "nsAutoPtr.h"
+#include "nsExpirationTracker.h"
+#include "nsHashKeys.h"
+#include "nsRefPtrHashtable.h"
+#include "nsSize.h"
+#include "nsTArray.h"
+#include "prsystem.h"
+#include "SVGImageContext.h"
+
+using std::max;
+using std::min;
+using mozilla::gfx::DrawTarget;
+
+/**
+ * Hashtable key class to use with objects for which Hash() and operator==()
+ * are defined.
+ * XXX(seth): This will get moved to xpcom/glue/nsHashKeys.h in a followup bug.
+ */
+template <typename T>
+class nsGenericHashKey : public PLDHashEntryHdr
+{
+public:
+  typedef const T& KeyType;
+  typedef const T* KeyTypePointer;
+
+  nsGenericHashKey(KeyTypePointer aKey) : mKey(*aKey) { }
+  nsGenericHashKey(const nsGenericHashKey<T>& aOther) : mKey(aOther.mKey) { }
+
+  KeyType GetKey() const { return mKey; }
+  bool KeyEquals(KeyTypePointer aKey) const { return *aKey == mKey; }
+
+  static KeyTypePointer KeyToPointer(KeyType aKey) { return &aKey; }
+  static PLDHashNumber HashKey(KeyTypePointer aKey) { return aKey->Hash(); }
+  enum { ALLOW_MEMMOVE = true };
+
+private:
+  T mKey;
+};
+
+namespace mozilla {
+namespace image {
+
+///////////////////////////////////////////////////////////////////////////////
+// SurfaceCache Implementation
+///////////////////////////////////////////////////////////////////////////////
+
+class CachedSurface;
+class SurfaceCacheImpl;
+
+/*
+ * 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 nsIntSize aSize)
+{
+  return aSize.width * aSize.height * 4;  // width * height * 4 bytes (32bpp)
+}
+
+/*
+ * 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(CachedSurface* aSurface, Cost aCost)
+    : mSurface(aSurface)
+    , mCost(aCost)
+  {
+    MOZ_ASSERT(aSurface, "Must have a surface");
+  }
+
+  CachedSurface* GetSurface() 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:
+  CachedSurface* mSurface;
+  Cost           mCost;
+};
+
+/*
+ * A CachedSurface associates a surface with a key that uniquely identifies that
+ * surface.
+ */
+class CachedSurface : public RefCounted<CachedSurface>
+{
+public:
+  CachedSurface(DrawTarget*       aTarget,
+                const nsIntSize   aTargetSize,
+                const Cost        aCost,
+                const ImageKey    aImageKey,
+                const SurfaceKey& aSurfaceKey)
+    : mTarget(aTarget)
+    , mTargetSize(aTargetSize)
+    , mCost(aCost)
+    , mImageKey(aImageKey)
+    , mSurfaceKey(aSurfaceKey)
+  {
+    MOZ_ASSERT(mTarget, "Must have a valid DrawTarget");
+    MOZ_ASSERT(mImageKey, "Must have a valid image key");
+  }
+
+  already_AddRefed<gfxDrawable> Drawable() const
+  {
+    nsRefPtr<gfxASurface> surface =
+      gfxPlatform::GetPlatform()->GetThebesSurfaceForDrawTarget(mTarget);
+    nsRefPtr<gfxDrawable> drawable = new gfxSurfaceDrawable(surface, mTargetSize);
+    return drawable.forget();
+  }
+
+  ImageKey GetImageKey() const { return mImageKey; }
+  SurfaceKey GetSurfaceKey() const { return mSurfaceKey; }
+  CostEntry GetCostEntry() { return image::CostEntry(this, mCost); }
+  nsExpirationState* GetExpirationState() { return &mExpirationState; }
+
+private:
+  nsExpirationState       mExpirationState;
+  nsRefPtr<DrawTarget>    mTarget;
+  const nsIntSize         mTargetSize;
+  const Cost              mCost;
+  const ImageKey          mImageKey;
+  const SurfaceKey        mSurfaceKey;
+};
+
+/*
+ * 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.
+ */
+class ImageSurfaceCache : public RefCounted<ImageSurfaceCache>
+{
+public:
+  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");
+    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");
+
+    mSurfaces.Remove(aSurface->GetSurfaceKey());
+  }
+
+  already_AddRefed<CachedSurface> Lookup(const SurfaceKey& aSurfaceKey)
+  {
+    nsRefPtr<CachedSurface> surface;
+    mSurfaces.Get(aSurfaceKey, getter_AddRefs(surface));
+    return surface.forget();
+  }
+
+  void ForEach(SurfaceTable::EnumReadFunction aFunction, void* aData)
+  {
+    mSurfaces.EnumerateRead(aFunction, aData);
+  }
+
+private:
+  SurfaceTable mSurfaces;
+};
+
+/*
+ * 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
+{
+public:
+  SurfaceCacheImpl(uint32_t aSurfaceCacheExpirationTimeMS,
+                   uint32_t aSurfaceCacheSize)
+    : mExpirationTracker(MOZ_THIS_IN_INITIALIZER_LIST(),
+                         aSurfaceCacheExpirationTimeMS)
+    , mMaxCost(aSurfaceCacheSize)
+    , mAvailableCost(aSurfaceCacheSize)
+  { }
+
+  void Insert(DrawTarget*       aTarget,
+              nsIntSize         aTargetSize,
+              const Cost        aCost,
+              const ImageKey    aImageKey,
+              const SurfaceKey& aSurfaceKey)
+  {
+    MOZ_ASSERT(!Lookup(aImageKey, aSurfaceKey).get(),
+               "Inserting a duplicate drawable into the SurfaceCache");
+
+    // If this is bigger than the maximum cache size, refuse to cache it.
+    if (!CanHold(aCost))
+      return;
+
+    nsRefPtr<CachedSurface> surface =
+      new CachedSurface(aTarget, aTargetSize, aCost, aImageKey, aSurfaceKey);
+
+    // Remove elements in order of cost until we can fit this in the cache.
+    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);
+    }
+
+    // Insert.
+    MOZ_ASSERT(aCost <= mAvailableCost, "Inserting despite too large a cost");
+    cache->Insert(aSurfaceKey, surface);
+    StartTracking(surface);
+  }
+
+  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()) {
+      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);
+  }
+
+  void StopTracking(CachedSurface* aSurface)
+  {
+    MOZ_ASSERT(aSurface, "Should have a surface");
+    CostEntry costEntry = aSurface->GetCostEntry();
+
+    mExpirationTracker.RemoveObject(aSurface);
+    DebugOnly<bool> foundInCosts = mCosts.RemoveElementSorted(costEntry);
+    mAvailableCost += costEntry.GetCost();
+
+    MOZ_ASSERT(foundInCosts, "Lost track of costs for this surface");
+    MOZ_ASSERT(mAvailableCost <= mMaxCost, "More available cost than we started with");
+  }
+
+  already_AddRefed<gfxDrawable> Lookup(const ImageKey    aImageKey,
+                                       const SurfaceKey& aSurfaceKey)
+  {
+    nsRefPtr<ImageSurfaceCache> cache = GetImageCache(aImageKey);
+    if (!cache)
+      return nullptr;  // No cached surfaces for this image.
+    
+    nsRefPtr<CachedSurface> surface = cache->Lookup(aSurfaceKey);
+    if (!surface)
+      return nullptr;  // Lookup in the per-image cache missed.
+    
+    mExpirationTracker.MarkUsed(surface);
+    return surface->Drawable();
+  }
+
+  bool CanHold(const Cost aCost) const
+  {
+    return aCost <= mMaxCost;
+  }
+
+  void Discard(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.
+    mImageCaches.Remove(aImageKey);
+  }
+
+  static PLDHashOperator DoStopTracking(const SurfaceKey&,
+                                        CachedSurface*    aSurface,
+                                        void*             aCache)
+  {
+    static_cast<SurfaceCacheImpl*>(aCache)->StopTracking(aSurface);
+    return PL_DHASH_NEXT;
+  }
+
+private:
+  already_AddRefed<ImageSurfaceCache> GetImageCache(const ImageKey aImageKey)
+  {
+    nsRefPtr<ImageSurfaceCache> imageCache;
+    mImageCaches.Get(aImageKey, getter_AddRefs(imageCache));
+    return imageCache.forget();
+  }
+
+  struct SurfaceTracker : public nsExpirationTracker<CachedSurface, 2>
+  {
+    SurfaceTracker(SurfaceCacheImpl* aCache, uint32_t aSurfaceCacheExpirationTimeMS)
+      : nsExpirationTracker(aSurfaceCacheExpirationTimeMS)
+      , mCache(aCache)
+    { }
+
+  protected:
+    virtual void NotifyExpired(CachedSurface* aSurface) MOZ_OVERRIDE
+    {
+      if (mCache) {
+        mCache->Remove(aSurface);
+      }
+    }
+
+  private:
+    SurfaceCacheImpl* const mCache;  // Weak pointer to owner.
+  };
+
+  nsTArray<CostEntry>                                       mCosts;
+  nsRefPtrHashtable<nsPtrHashKey<Image>, ImageSurfaceCache> mImageCaches;
+  SurfaceTracker                                            mExpirationTracker;
+  const Cost                                                mMaxCost;
+  Cost                                                      mAvailableCost;
+};
+
+
+///////////////////////////////////////////////////////////////////////////////
+// Static Data
+///////////////////////////////////////////////////////////////////////////////
+
+// The single surface cache instance.
+static SurfaceCacheImpl* sInstance = nullptr;
+
+
+///////////////////////////////////////////////////////////////////////////////
+// Public API
+///////////////////////////////////////////////////////////////////////////////
+
+/* static */ void
+SurfaceCache::Initialize()
+{
+  // Initialize preferences.
+  MOZ_ASSERT(!sInstance, "Shouldn't initialize more than once");
+
+  // Length of time before an unused surface is removed from the cache, in milliseconds.
+  // The default value gives an expiration time of 1 minute.
+  uint32_t surfaceCacheExpirationTimeMS =
+    Preferences::GetUint("image.mem.surfacecache.min_expiration_ms", 60 * 1000);
+
+  // Maximum size of the surface cache, in kilobytes.
+  // The default is 100MB. (But we may override this for e.g. B2G.)
+  uint32_t surfaceCacheMaxSizeKB =
+    Preferences::GetUint("image.mem.surfacecache.max_size_kb", 100 * 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.
+  // The default value is 64, which yields a 64MB cache on a 4GB 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.
+  uint32_t surfaceCacheSizeFactor =
+    Preferences::GetUint("image.mem.surfacecache.size_factor", 64);
+
+  // 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);
+
+  // 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);
+}
+
+/* static */ void
+SurfaceCache::Shutdown()
+{
+  MOZ_ASSERT(sInstance, "No singleton - was Shutdown() called twice?");
+  delete sInstance;
+  sInstance = nullptr;
+}
+
+/* static */ already_AddRefed<gfxDrawable>
+SurfaceCache::Lookup(const ImageKey    aImageKey,
+                     const SurfaceKey& aSurfaceKey)
+{
+  MOZ_ASSERT(sInstance, "Should be initialized");
+  MOZ_ASSERT(NS_IsMainThread());
+
+  return sInstance->Lookup(aImageKey, aSurfaceKey);
+}
+
+/* static */ void
+SurfaceCache::Insert(DrawTarget*       aTarget,
+                     const ImageKey    aImageKey,
+                     const SurfaceKey& aSurfaceKey)
+{
+  MOZ_ASSERT(sInstance, "Should be initialized");
+  MOZ_ASSERT(NS_IsMainThread());
+
+  Cost cost = ComputeCost(aSurfaceKey.Size());
+  return sInstance->Insert(aTarget, aSurfaceKey.Size(), cost, aImageKey, aSurfaceKey);
+}
+
+/* static */ bool
+SurfaceCache::CanHold(const nsIntSize& aSize)
+{
+  MOZ_ASSERT(sInstance, "Should be initialized");
+  MOZ_ASSERT(NS_IsMainThread());
+
+  Cost cost = ComputeCost(aSize);
+  return sInstance->CanHold(cost);
+}
+
+/* static */ void
+SurfaceCache::Discard(Image* aImageKey)
+{
+  MOZ_ASSERT(sInstance, "Should be initialized");
+  MOZ_ASSERT(NS_IsMainThread());
+
+  return sInstance->Discard(aImageKey);
+}
+
+} // namespace image
+} // namespace mozilla
new file mode 100644
--- /dev/null
+++ b/image/src/SurfaceCache.h
@@ -0,0 +1,172 @@
+/* -*- 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.
+ */
+
+#ifndef MOZILLA_IMAGELIB_SURFACECACHE_H_
+#define MOZILLA_IMAGELIB_SURFACECACHE_H_
+
+#include "mozilla/HashFunctions.h"  // for HashGeneric and AddToHash
+#include "gfxPoint.h"               // for gfxSize
+#include "nsCOMPtr.h"               // for already_AddRefed
+#include "nsSize.h"                 // for nsIntSize
+#include "SVGImageContext.h"        // for SVGImageContext
+
+class gfxDrawable;
+
+namespace mozilla {
+
+namespace gfx {
+class DrawTarget;
+} // namespace gfx
+
+namespace image {
+
+class Image;
+
+/*
+ * ImageKey contains the information we need to look up all cached surfaces for
+ * a particular image.
+ */
+typedef Image* ImageKey;
+
+/*
+ * SurfaceKey contains the information we need to look up a specific cached
+ * surface. Together with an ImageKey, this uniquely identifies the surface.
+ *
+ * XXX(seth): Right now this is specialized to the needs of VectorImage. We'll
+ * generalize it in bug 919071.
+ */
+class SurfaceKey
+{
+public:
+  SurfaceKey(const nsIntSize aSize,
+             const gfxSize aScale,
+             const SVGImageContext* aSVGContext,
+             const float aAnimationTime,
+             const uint32_t aFlags)
+    : mSize(aSize)
+    , mScale(aScale)
+    , mSVGContextIsValid(aSVGContext != nullptr)
+    , mAnimationTime(aAnimationTime)
+    , mFlags(aFlags)
+  {
+    // XXX(seth): Would love to use Maybe<T> here, but see bug 913586.
+    if (mSVGContextIsValid)
+      mSVGContext = *aSVGContext;
+  }
+
+  bool operator==(const SurfaceKey& aOther) const
+  {
+    bool matchesSVGContext = aOther.mSVGContextIsValid == mSVGContextIsValid &&
+                             (!mSVGContextIsValid || aOther.mSVGContext == mSVGContext);
+    return aOther.mSize == mSize &&
+           aOther.mScale == mScale &&
+           matchesSVGContext &&
+           aOther.mAnimationTime == mAnimationTime &&
+           aOther.mFlags == mFlags;
+  }
+
+  uint32_t Hash() const
+  {
+    uint32_t hash = HashGeneric(mSize.width, mSize.height);
+    hash = AddToHash(hash, mScale.width, mScale.height);
+    hash = AddToHash(hash, mSVGContextIsValid, mSVGContext.Hash());
+    hash = AddToHash(hash, mAnimationTime, mFlags);
+    return hash;
+  }
+
+  nsIntSize Size() const { return mSize; }
+
+private:
+  nsIntSize       mSize;
+  gfxSize         mScale;
+  SVGImageContext mSVGContext;
+  bool            mSVGContextIsValid;
+  float           mAnimationTime;
+  uint32_t        mFlags;
+};
+
+/**
+ * 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.
+ *
+ * SurfaceCache is not thread-safe; it should only be accessed from the main
+ * thread.
+ */
+struct SurfaceCache
+{
+  /*
+   * Initialize static data. Called during imagelib module initialization.
+   */
+  static void Initialize();
+
+  /*
+   * Release static data. Called during imagelib module shutdown.
+   */
+  static void Shutdown();
+
+  /*
+   * Look up a surface in the cache.
+   *
+   * @param aImageKey    Key data identifying which image the surface belongs to.
+   * @param aSurfaceKey  Key data which uniquely identifies the requested surface.
+   *
+   * @return the requested surface, or nullptr if not found.
+   */
+  static already_AddRefed<gfxDrawable> 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.
+   *
+   * @param aTarget      The new surface (in the form of a DrawTarget) 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.
+   */
+  static void Insert(mozilla::gfx::DrawTarget* aTarget,
+                     const ImageKey            aImageKey,
+                     const SurfaceKey&         aSurfaceKey);
+
+  /*
+   * 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 nsIntSize& aSize);
+
+  /*
+   * 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.
+   *
+   * @param aImageKey  The image which should be removed from the cache.
+   */
+  static void Discard(const ImageKey aImageKey);
+
+private:
+  virtual ~SurfaceCache() = 0;  // Forbid instantiation.
+};
+
+} // namespace image
+} // namespace mozilla
+
+#endif // MOZILLA_IMAGELIB_SURFACECACHE_H_
--- a/image/src/moz.build
+++ b/image/src/moz.build
@@ -26,16 +26,17 @@ CPP_SOURCES += [
     'ImageFactory.cpp',
     'ImageMetadata.cpp',
     'ImageOps.cpp',
     'ImageWrapper.cpp',
     'OrientedImage.cpp',
     'RasterImage.cpp',
     'SVGDocumentWrapper.cpp',
     'ScriptedNotificationObserver.cpp',
+    'SurfaceCache.cpp',
     'VectorImage.cpp',
     'imgFrame.cpp',
     'imgLoader.cpp',
     'imgRequest.cpp',
     'imgRequestProxy.cpp',
     'imgStatusTracker.cpp',
     'imgTools.cpp',
 ]
--- a/modules/libpref/src/init/all.js
+++ b/modules/libpref/src/init/all.js
@@ -4082,16 +4082,30 @@ pref("image.mem.decode_bytes_at_a_time",
 
 // The longest time we can spend in an iteration of an async decode
 pref("image.mem.max_ms_before_yield", 5);
 
 // The maximum amount of decoded image data we'll willingly keep around (we
 // might keep around more than this, but we'll try to get down to this value).
 pref("image.mem.max_decoded_image_kb", 51200);
 
+// Minimum timeout for expiring unused images from the surface cache, in
+// milliseconds. This controls how long we store cached temporary surfaces.
+pref("image.mem.surfacecache.min_expiration_ms", 60000); // 60ms
+
+// Maximum size for the surface cache, in kilobytes.
+pref("image.mem.surfacecache.max_size_kb", 102400); // 100MB
+
+// The surface cache's size, within the constraints of the maximum size set
+// above, is determined using a formula based on system capabilities like memory
+// size. The size factor is used to tune this formula. Larger size factors
+// result in smaller caches. The default should be a good balance for most
+// systems.
+pref("image.mem.surfacecache.size_factor", 64);
+
 // Whether we decode images on multiple background threads rather than the
 // foreground thread.
 pref("image.multithreaded_decoding.enabled", true);
 
 // How many threads we'll use for multithreaded decoding. If < 0, will be
 // automatically determined based on the system's number of cores.
 pref("image.multithreaded_decoding.limit", -1);