Bug 1293472 (Part 3) - Store animated images in the surface cache as a sequence of frames, rather than each frame getting its own cache entry. r=dholbert,edwin,njn
authorSeth Fowler <mark.seth.fowler@gmail.com>
Thu, 18 Aug 2016 00:06:41 -0700
changeset 351488 cfb1f1eeceb36952195fd255d5a7a5f017771cbd
parent 351487 02f9e27b988dd63dce023fbca505ac74a1e8a1f6
child 351489 856b1b82372a8273af0c327bb5b6ce5e8e7ff359
push id6570
push userraliiev@mozilla.com
push dateMon, 14 Nov 2016 12:26:13 +0000
treeherdermozilla-beta@f455459b2ae5 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersdholbert, edwin, njn
bugs1293472
milestone51.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 1293472 (Part 3) - Store animated images in the surface cache as a sequence of frames, rather than each frame getting its own cache entry. r=dholbert,edwin,njn
image/AnimationSurfaceProvider.cpp
image/AnimationSurfaceProvider.h
image/DecodedSurfaceProvider.cpp
image/Decoder.cpp
image/Decoder.h
image/DecoderFactory.cpp
image/FrameAnimator.cpp
image/IDecodingTask.cpp
image/IDecodingTask.h
image/ISurfaceProvider.h
image/Image.h
image/PlaybackType.h
image/RasterImage.cpp
image/RasterImage.h
image/SurfaceCache.cpp
image/SurfaceCache.h
image/VectorImage.cpp
image/imgLoader.cpp
image/test/gtest/TestDecoders.cpp
image/test/gtest/TestMetadata.cpp
image/test/mochitest/mochitest.ini
--- a/image/AnimationSurfaceProvider.cpp
+++ b/image/AnimationSurfaceProvider.cpp
@@ -111,16 +111,31 @@ AnimationSurfaceProvider::LogicalSizeInB
   // Unfortunately there's no way to know in advance how many frames an
   // animation has, so we really can't do better here. This will become correct
   // once bug 1289954 is complete.
   IntSize size = mSurfaceKey.Size();
   return 3 * size.width * size.height * sizeof(uint32_t);
 }
 
 void
+AnimationSurfaceProvider::AddSizeOfExcludingThis(MallocSizeOf aMallocSizeOf,
+                                                 size_t& aHeapSizeOut,
+                                                 size_t& aNonHeapSizeOut)
+{
+  // Note that the surface cache lock is already held here, and then we acquire
+  // mFramesMutex. For this method, this ordering is unavoidable, which means
+  // that we must be careful to always use the same ordering elsewhere.
+  MutexAutoLock lock(mFramesMutex);
+
+  for (const RawAccessFrameRef& frame : mFrames) {
+    frame->AddSizeOfExcludingThis(aMallocSizeOf, aHeapSizeOut, aNonHeapSizeOut);
+  }
+}
+
+void
 AnimationSurfaceProvider::Run()
 {
   MutexAutoLock lock(mDecodingMutex);
 
   if (!mDecoder || !mImage) {
     MOZ_ASSERT_UNREACHABLE("Running after decoding finished?");
     return;
   }
@@ -228,17 +243,21 @@ AnimationSurfaceProvider::CheckForNewFra
 }
 
 void
 AnimationSurfaceProvider::AnnounceSurfaceAvailable()
 {
   mFramesMutex.AssertNotCurrentThreadOwns();
   MOZ_ASSERT(mImage);
 
-  // We just got the first frame; let the surface cache know.
+  // We just got the first frame; let the surface cache know. We deliberately do
+  // this outside of mFramesMutex to avoid a potential deadlock with
+  // AddSizeOfExcludingThis(), since otherwise we'd be acquiring mFramesMutex
+  // and then the surface cache lock, while the memory reporting code would
+  // acquire the surface cache lock and then mFramesMutex.
   SurfaceCache::SurfaceAvailable(WrapNotNull(this),
                                  ImageKey(mImage.get()),
                                  mSurfaceKey);
 }
 
 void
 AnimationSurfaceProvider::FinishDecoding()
 {
--- a/image/AnimationSurfaceProvider.h
+++ b/image/AnimationSurfaceProvider.h
@@ -40,16 +40,19 @@ public:
 
 public:
   // We use the ISurfaceProvider constructor of DrawableSurface to indicate that
   // our surfaces are computed lazily.
   DrawableSurface Surface() override { return DrawableSurface(WrapNotNull(this)); }
 
   bool IsFinished() const override;
   size_t LogicalSizeInBytes() const override;
+  void AddSizeOfExcludingThis(MallocSizeOf aMallocSizeOf,
+                              size_t& aHeapSizeOut,
+                              size_t& aNonHeapSizeOut) override;
 
 protected:
   DrawableFrameRef DrawableRef(size_t aFrame) override;
 
   // Animation frames are always locked. This is because we only want to release
   // their memory atomically (due to the surface cache discarding them). If they
   // were unlocked, the OS could end up releasing the memory of random frames
   // from the middle of the animation, which is not worth the complexity of
--- a/image/DecodedSurfaceProvider.cpp
+++ b/image/DecodedSurfaceProvider.cpp
@@ -22,17 +22,17 @@ DecodedSurfaceProvider::DecodedSurfacePr
   , mImage(aImage.get())
   , mMutex("mozilla::image::DecodedSurfaceProvider")
   , mDecoder(aDecoder.get())
   , mSurfaceKey(aSurfaceKey)
 {
   MOZ_ASSERT(!mDecoder->IsMetadataDecode(),
              "Use MetadataDecodingTask for metadata decodes");
   MOZ_ASSERT(mDecoder->IsFirstFrameDecode(),
-             "Use AnimationDecodingTask for animation decodes");
+             "Use AnimationSurfaceProvider for animation decodes");
 }
 
 DecodedSurfaceProvider::~DecodedSurfaceProvider()
 {
   DropImageReference();
 }
 
 void
--- a/image/Decoder.cpp
+++ b/image/Decoder.cpp
@@ -93,20 +93,19 @@ Decoder::Init()
   MOZ_ASSERT(!mInitialized, "Can't re-initialize a decoder!");
 
   // All decoders must have a SourceBufferIterator.
   MOZ_ASSERT(mIterator);
 
   // Metadata decoders must not set an output size.
   MOZ_ASSERT_IF(mMetadataDecode, !mHaveExplicitOutputSize);
 
-  // It doesn't make sense to decode anything but the first frame if we can't
-  // store anything in the SurfaceCache, since only the last frame we decode
-  // will be retrievable.
-  MOZ_ASSERT(ShouldUseSurfaceCache() || IsFirstFrameDecode());
+  // All decoders must be anonymous except for metadata decoders.
+  // XXX(seth): Soon that exception will be removed.
+  MOZ_ASSERT_IF(mImage, IsMetadataDecode());
 
   // Implementation-specific initialization.
   nsresult rv = InitInternal();
 
   mInitialized = true;
 
   return rv;
 }
@@ -336,60 +335,30 @@ Decoder::AllocateFrameInternal(uint32_t 
   }
 
   if (aOutputSize.width <= 0 || aOutputSize.height <= 0 ||
       aFrameRect.width <= 0 || aFrameRect.height <= 0) {
     NS_WARNING("Trying to add frame with zero or negative size");
     return RawAccessFrameRef();
   }
 
-  const uint32_t bytesPerPixel = aPaletteDepth == 0 ? 4 : 1;
-  if (ShouldUseSurfaceCache() &&
-      !SurfaceCache::CanHold(aFrameRect.Size(), bytesPerPixel)) {
-    NS_WARNING("Trying to add frame that's too large for the SurfaceCache");
-    return RawAccessFrameRef();
-  }
-
   NotNull<RefPtr<imgFrame>> frame = WrapNotNull(new imgFrame());
   bool nonPremult = bool(mSurfaceFlags & SurfaceFlags::NO_PREMULTIPLY_ALPHA);
   if (NS_FAILED(frame->InitForDecoder(aOutputSize, aFrameRect, aFormat,
                                       aPaletteDepth, nonPremult))) {
     NS_WARNING("imgFrame::Init should succeed");
     return RawAccessFrameRef();
   }
 
   RawAccessFrameRef ref = frame->RawAccessRef();
   if (!ref) {
     frame->Abort();
     return RawAccessFrameRef();
   }
 
-  if (ShouldUseSurfaceCache()) {
-    NotNull<RefPtr<ISurfaceProvider>> provider =
-      WrapNotNull(new SimpleSurfaceProvider(frame));
-    InsertOutcome outcome =
-      SurfaceCache::Insert(provider, ImageKey(mImage.get()),
-                           RasterSurfaceKey(aOutputSize,
-                                            mSurfaceFlags,
-                                            aFrameNum));
-    if (outcome == InsertOutcome::FAILURE) {
-      // We couldn't insert the surface, almost certainly due to low memory. We
-      // treat this as a permanent error to help the system recover; otherwise,
-      // we might just end up attempting to decode this image again immediately.
-      ref->Abort();
-      return RawAccessFrameRef();
-    } else if (outcome == InsertOutcome::FAILURE_ALREADY_PRESENT) {
-      // Another decoder beat us to decoding this frame. We abort this decoder
-      // rather than treat this as a real error.
-      mDecodeAborted = true;
-      ref->Abort();
-      return RawAccessFrameRef();
-    }
-  }
-
   if (aFrameNum == 1) {
     MOZ_ASSERT(aPreviousFrame, "Must provide a previous frame when animated");
     aPreviousFrame->SetRawAccessOnly();
 
     // If we dispose of the first frame by clearing it, then the first frame's
     // refresh area is all of itself.
     // RESTORE_PREVIOUS is invalid (assumed to be DISPOSE_CLEAR).
     AnimationData previousFrameData = aPreviousFrame->GetAnimationData();
--- a/image/Decoder.h
+++ b/image/Decoder.h
@@ -282,19 +282,16 @@ public:
   {
     return mReachedTerminalState || mDecodeDone ||
            (mMetadataDecode && HasSize()) || HasError();
   }
 
   /// Are we in the middle of a frame right now? Used for assertions only.
   bool InFrame() const { return mInFrame; }
 
-  /// Should we store surfaces created by this decoder in the SurfaceCache?
-  bool ShouldUseSurfaceCache() const { return bool(mImage); }
-
   /**
    * Returns true if this decoder was aborted.
    *
    * This may happen due to a low-memory condition, or because another decoder
    * was racing with this one to decode the same frames with the same flags and
    * this decoder lost the race. Either way, this is not a permanent situation
    * and does not constitute an error, so we don't report any errors when this
    * happens.
--- a/image/DecoderFactory.cpp
+++ b/image/DecoderFactory.cpp
@@ -3,16 +3,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 "DecoderFactory.h"
 
 #include "nsMimeTypes.h"
 #include "mozilla/RefPtr.h"
 
+#include "AnimationSurfaceProvider.h"
 #include "Decoder.h"
 #include "IDecodingTask.h"
 #include "nsPNGDecoder.h"
 #include "nsGIFDecoder2.h"
 #include "nsJPEGDecoder.h"
 #include "nsBMPDecoder.h"
 #include "nsICODecoder.h"
 #include "nsIconDecoder.h"
@@ -136,17 +137,17 @@ DecoderFactory::CreateDecoder(DecoderTyp
 
   if (NS_FAILED(decoder->Init())) {
     return nullptr;
   }
 
   // Create a DecodedSurfaceProvider which will manage the decoding process and
   // make this decoder's output available in the surface cache.
   SurfaceKey surfaceKey =
-    RasterSurfaceKey(aOutputSize, aSurfaceFlags, /* aFrameNum = */ 0);
+    RasterSurfaceKey(aOutputSize, aSurfaceFlags, PlaybackType::eStatic);
   NotNull<RefPtr<DecodedSurfaceProvider>> provider =
     WrapNotNull(new DecodedSurfaceProvider(aImage,
                                            WrapNotNull(decoder),
                                            surfaceKey));
 
   // Attempt to insert the surface provider into the surface cache right away so
   // we won't trigger any more decoders with the same parameters.
   InsertOutcome outcome =
@@ -170,41 +171,50 @@ DecoderFactory::CreateAnimationDecoder(D
 {
   if (aType == DecoderType::UNKNOWN) {
     return nullptr;
   }
 
   MOZ_ASSERT(aType == DecoderType::GIF || aType == DecoderType::PNG,
              "Calling CreateAnimationDecoder for non-animating DecoderType");
 
-  RefPtr<Decoder> decoder =
-    GetDecoder(aType, aImage, /* aIsRedecode = */ true);
+  // Create an anonymous decoder. Interaction with the SurfaceCache and the
+  // owning RasterImage will be mediated by AnimationSurfaceProvider.
+  RefPtr<Decoder> decoder = GetDecoder(aType, nullptr, /* aIsRedecode = */ true);
   MOZ_ASSERT(decoder, "Should have a decoder now");
 
   // Initialize the decoder.
   decoder->SetMetadataDecode(false);
   decoder->SetIterator(aSourceBuffer->Iterator());
   decoder->SetDecoderFlags(aDecoderFlags | DecoderFlags::IS_REDECODE);
   decoder->SetSurfaceFlags(aSurfaceFlags);
 
   if (NS_FAILED(decoder->Init())) {
     return nullptr;
   }
 
-  // Add a placeholder for the first frame to the SurfaceCache so we won't
-  // trigger any more decoders with the same parameters.
+  // Create an AnimationSurfaceProvider which will manage the decoding process
+  // and make this decoder's output available in the surface cache.
   SurfaceKey surfaceKey =
-    RasterSurfaceKey(aIntrinsicSize, aSurfaceFlags, /* aFrameNum = */ 0);
+    RasterSurfaceKey(aIntrinsicSize, aSurfaceFlags, PlaybackType::eAnimated);
+  NotNull<RefPtr<AnimationSurfaceProvider>> provider =
+    WrapNotNull(new AnimationSurfaceProvider(aImage,
+                                             WrapNotNull(decoder),
+                                             surfaceKey));
+
+  // Attempt to insert the surface provider into the surface cache right away so
+  // we won't trigger any more decoders with the same parameters.
   InsertOutcome outcome =
-    SurfaceCache::InsertPlaceholder(ImageKey(aImage.get()), surfaceKey);
+    SurfaceCache::Insert(provider, ImageKey(aImage.get()), surfaceKey);
   if (outcome != InsertOutcome::SUCCESS) {
     return nullptr;
   }
 
-  RefPtr<IDecodingTask> task = new AnimationDecodingTask(WrapNotNull(decoder));
+  // Return the surface provider in its IDecodingTask guise.
+  RefPtr<IDecodingTask> task = provider.get();
   return task.forget();
 }
 
 /* static */ already_AddRefed<IDecodingTask>
 DecoderFactory::CreateMetadataDecoder(DecoderType aType,
                                       NotNull<RasterImage*> aImage,
                                       NotNull<SourceBuffer*> aSourceBuffer,
                                       int aSampleSize)
--- a/image/FrameAnimator.cpp
+++ b/image/FrameAnimator.cpp
@@ -292,33 +292,42 @@ FrameAnimator::RequestRefresh(AnimationS
   }
 
   return ret;
 }
 
 LookupResult
 FrameAnimator::GetCompositedFrame(uint32_t aFrameNum)
 {
-  MOZ_ASSERT(aFrameNum != 0, "First frame is never composited");
-
   // If we have a composited version of this frame, return that.
   if (mLastCompositedFrameIndex == int32_t(aFrameNum)) {
     return LookupResult(DrawableSurface(mCompositingFrame->DrawableRef()),
                         MatchType::EXACT);
   }
 
   // Otherwise return the raw frame. DoBlend is required to ensure that we only
   // hit this case if the frame is not paletted and doesn't require compositing.
   LookupResult result =
     SurfaceCache::Lookup(ImageKey(mImage),
                          RasterSurfaceKey(mSize,
                                           DefaultSurfaceFlags(),
-                                          aFrameNum));
-  MOZ_ASSERT(!result || !result.Surface()->GetIsPaletted(),
+                                          PlaybackType::eAnimated));
+  if (!result) {
+    return result;
+  }
+
+  // Seek to the appropriate frame. If seeking fails, it means that we couldn't
+  // get the frame we're looking for; treat this as if the lookup failed.
+  if (NS_FAILED(result.Surface().Seek(aFrameNum))) {
+    return LookupResult(MatchType::NOT_FOUND);
+  }
+
+  MOZ_ASSERT(!result.Surface()->GetIsPaletted(),
              "About to return a paletted frame");
+
   return result;
 }
 
 FrameTimeout
 FrameAnimator::GetTimeoutForFrame(uint32_t aFrameNum) const
 {
   RawAccessFrameRef frame = GetRawFrame(aFrameNum);
   if (frame) {
@@ -334,17 +343,17 @@ static void
 DoCollectSizeOfCompositingSurfaces(const RawAccessFrameRef& aSurface,
                                    SurfaceMemoryCounterType aType,
                                    nsTArray<SurfaceMemoryCounter>& aCounters,
                                    MallocSizeOf aMallocSizeOf)
 {
   // Concoct a SurfaceKey for this surface.
   SurfaceKey key = RasterSurfaceKey(aSurface->GetImageSize(),
                                     DefaultSurfaceFlags(),
-                                    /* aFrameNum = */ 0);
+                                    PlaybackType::eStatic);
 
   // Create a counter for this surface.
   SurfaceMemoryCounter counter(key, /* aIsLocked = */ true, aType);
 
   // Extract the surface's memory usage information.
   size_t heap = 0, nonHeap = 0;
   aSurface->AddSizeOfExcludingThis(aMallocSizeOf, heap, nonHeap);
   counter.Values().SetDecodedHeap(heap);
@@ -376,19 +385,29 @@ FrameAnimator::CollectSizeOfCompositingS
 
 RawAccessFrameRef
 FrameAnimator::GetRawFrame(uint32_t aFrameNum) const
 {
   LookupResult result =
     SurfaceCache::Lookup(ImageKey(mImage),
                          RasterSurfaceKey(mSize,
                                           DefaultSurfaceFlags(),
-                                          aFrameNum));
-  return result ? result.Surface()->RawAccessRef()
-                : RawAccessFrameRef();
+                                          PlaybackType::eAnimated));
+  if (!result) {
+    return RawAccessFrameRef();
+  }
+
+  // Seek to the frame we want. If seeking fails, it means we couldn't get the
+  // frame we're looking for, so we bail here to avoid returning the wrong frame
+  // to the caller.
+  if (NS_FAILED(result.Surface().Seek(aFrameNum))) {
+    return RawAccessFrameRef();  // Not available yet.
+  }
+
+  return result.Surface()->RawAccessRef();
 }
 
 //******************************************************************************
 // DoBlend gets called when the timer for animation get fired and we have to
 // update the composited frame of the animation.
 bool
 FrameAnimator::DoBlend(IntRect* aDirtyRect,
                        uint32_t aPrevFrameIndex,
--- a/image/IDecodingTask.cpp
+++ b/image/IDecodingTask.cpp
@@ -33,31 +33,31 @@ IDecodingTask::NotifyProgress(NotNull<Ra
   // important that we don't wait until the lambda actually runs to capture the
   // state that we're going to notify. That would both introduce data races on
   // the decoder's state and cause inconsistencies between the NotifyProgress()
   // calls we make off-main-thread and the notifications that RasterImage
   // actually receives, which would cause bugs.
   Progress progress = aDecoder->TakeProgress();
   IntRect invalidRect = aDecoder->TakeInvalidRect();
   Maybe<uint32_t> frameCount = aDecoder->TakeCompleteFrameCount();
+  DecoderFlags decoderFlags = aDecoder->GetDecoderFlags();
   SurfaceFlags surfaceFlags = aDecoder->GetSurfaceFlags();
 
   // Synchronously notify if we can.
-  if (NS_IsMainThread() &&
-      !(aDecoder->GetDecoderFlags() & DecoderFlags::ASYNC_NOTIFY)) {
-    aImage->NotifyProgress(progress, invalidRect,
-                           frameCount, surfaceFlags);
+  if (NS_IsMainThread() && !(decoderFlags & DecoderFlags::ASYNC_NOTIFY)) {
+    aImage->NotifyProgress(progress, invalidRect, frameCount,
+                           decoderFlags, surfaceFlags);
     return;
   }
 
   // We're forced to notify asynchronously.
   NotNull<RefPtr<RasterImage>> image = aImage;
   NS_DispatchToMainThread(NS_NewRunnableFunction([=]() -> void {
-    image->NotifyProgress(progress, invalidRect,
-                          frameCount, surfaceFlags);
+    image->NotifyProgress(progress, invalidRect, frameCount,
+                          decoderFlags, surfaceFlags);
   }));
 }
 
 /* static */ void
 IDecodingTask::NotifyDecodeComplete(NotNull<RasterImage*> aImage,
                                     NotNull<Decoder*> aDecoder)
 {
   MOZ_ASSERT(aDecoder->HasError() || !aDecoder->InFrame(),
@@ -65,100 +65,49 @@ IDecodingTask::NotifyDecodeComplete(NotN
 
   // Capture the decoder's state.
   DecoderFinalStatus finalStatus = aDecoder->FinalStatus();
   ImageMetadata metadata = aDecoder->GetImageMetadata();
   DecoderTelemetry telemetry = aDecoder->Telemetry();
   Progress progress = aDecoder->TakeProgress();
   IntRect invalidRect = aDecoder->TakeInvalidRect();
   Maybe<uint32_t> frameCount = aDecoder->TakeCompleteFrameCount();
+  DecoderFlags decoderFlags = aDecoder->GetDecoderFlags();
   SurfaceFlags surfaceFlags = aDecoder->GetSurfaceFlags();
 
   // Synchronously notify if we can.
-  if (NS_IsMainThread() &&
-      !(aDecoder->GetDecoderFlags() & DecoderFlags::ASYNC_NOTIFY)) {
+  if (NS_IsMainThread() && !(decoderFlags & DecoderFlags::ASYNC_NOTIFY)) {
     aImage->NotifyDecodeComplete(finalStatus, metadata, telemetry, progress,
-                                 invalidRect, frameCount, surfaceFlags);
+                                 invalidRect, frameCount, decoderFlags,
+                                 surfaceFlags);
     return;
   }
 
   // We're forced to notify asynchronously.
   NotNull<RefPtr<RasterImage>> image = aImage;
   NS_DispatchToMainThread(NS_NewRunnableFunction([=]() -> void {
     image->NotifyDecodeComplete(finalStatus, metadata, telemetry, progress,
-                                invalidRect, frameCount, surfaceFlags);
+                                invalidRect, frameCount, decoderFlags,
+                                surfaceFlags);
   }));
 }
 
 
 ///////////////////////////////////////////////////////////////////////////////
 // IDecodingTask implementation.
 ///////////////////////////////////////////////////////////////////////////////
 
 void
 IDecodingTask::Resume()
 {
   DecodePool::Singleton()->AsyncRun(this);
 }
 
 
 ///////////////////////////////////////////////////////////////////////////////
-// AnimationDecodingTask implementation.
-///////////////////////////////////////////////////////////////////////////////
-
-AnimationDecodingTask::AnimationDecodingTask(NotNull<Decoder*> aDecoder)
-  : mMutex("mozilla::image::AnimationDecodingTask")
-  , mDecoder(aDecoder)
-{
-  MOZ_ASSERT(!mDecoder->IsMetadataDecode(),
-             "Use MetadataDecodingTask for metadata decodes");
-  MOZ_ASSERT(!mDecoder->IsFirstFrameDecode(),
-             "Use DecodingTask for single-frame image decodes");
-}
-
-void
-AnimationDecodingTask::Run()
-{
-  MutexAutoLock lock(mMutex);
-
-  while (true) {
-    LexerResult result = mDecoder->Decode(WrapNotNull(this));
-
-    if (result.is<TerminalState>()) {
-      NotifyDecodeComplete(mDecoder->GetImage(), mDecoder);
-      return;  // We're done.
-    }
-
-    MOZ_ASSERT(result.is<Yield>());
-
-    // Notify for the progress we've made so far.
-    if (mDecoder->HasProgress()) {
-      NotifyProgress(mDecoder->GetImage(), mDecoder);
-    }
-
-    if (result == LexerResult(Yield::NEED_MORE_DATA)) {
-      // We can't make any more progress right now. The decoder itself will
-      // ensure that we get reenqueued when more data is available; just return
-      // for now.
-      return;
-    }
-
-    // Right now we don't do anything special for other kinds of yields, so just
-    // keep working.
-  }
-}
-
-bool
-AnimationDecodingTask::ShouldPreferSyncRun() const
-{
-  return mDecoder->ShouldSyncDecode(gfxPrefs::ImageMemDecodeBytesAtATime());
-}
-
-
-///////////////////////////////////////////////////////////////////////////////
 // MetadataDecodingTask implementation.
 ///////////////////////////////////////////////////////////////////////////////
 
 MetadataDecodingTask::MetadataDecodingTask(NotNull<Decoder*> aDecoder)
   : mMutex("mozilla::image::MetadataDecodingTask")
   , mDecoder(aDecoder)
 {
   MOZ_ASSERT(mDecoder->IsMetadataDecode(),
--- a/image/IDecodingTask.h
+++ b/image/IDecodingTask.h
@@ -58,43 +58,16 @@ protected:
   static void NotifyDecodeComplete(NotNull<RasterImage*> aImage,
                                    NotNull<Decoder*> aDecoder);
 
   virtual ~IDecodingTask() { }
 };
 
 
 /**
- * An IDecodingTask implementation for full decodes of animated images.
- */
-class AnimationDecodingTask final : public IDecodingTask
-{
-public:
-  NS_INLINE_DECL_THREADSAFE_REFCOUNTING(AnimationDecodingTask, override)
-
-  explicit AnimationDecodingTask(NotNull<Decoder*> aDecoder);
-
-  void Run() override;
-  bool ShouldPreferSyncRun() const override;
-
-  // Full decodes are low priority compared to metadata decodes because they
-  // don't block layout or page load.
-  TaskPriority Priority() const override { return TaskPriority::eLow; }
-
-private:
-  virtual ~AnimationDecodingTask() { }
-
-  /// Mutex protecting access to mDecoder.
-  Mutex mMutex;
-
-  NotNull<RefPtr<Decoder>> mDecoder;
-};
-
-
-/**
  * An IDecodingTask implementation for metadata decodes of images.
  */
 class MetadataDecodingTask final : public IDecodingTask
 {
 public:
   NS_INLINE_DECL_THREADSAFE_REFCOUNTING(MetadataDecodingTask, override)
 
   explicit MetadataDecodingTask(NotNull<Decoder*> aDecoder);
--- a/image/ISurfaceProvider.h
+++ b/image/ISurfaceProvider.h
@@ -8,16 +8,17 @@
  * generate one, and various implementations.
  */
 
 #ifndef mozilla_image_ISurfaceProvider_h
 #define mozilla_image_ISurfaceProvider_h
 
 #include "mozilla/Attributes.h"
 #include "mozilla/Maybe.h"
+#include "mozilla/MemoryReporting.h"
 #include "mozilla/NotNull.h"
 #include "mozilla/TimeStamp.h"
 #include "mozilla/Variant.h"
 #include "mozilla/gfx/2D.h"
 
 #include "imgFrame.h"
 #include "SurfaceCache.h"
 
@@ -45,16 +46,31 @@ public:
   /// @return true if DrawableRef() will return a completely decoded surface.
   virtual bool IsFinished() const = 0;
 
   /// @return the number of bytes of memory this ISurfaceProvider is expected to
   /// require. Optimizations may result in lower real memory usage. Trivial
   /// overhead is ignored.
   virtual size_t LogicalSizeInBytes() const = 0;
 
+  /// @return the actual number of bytes of memory this ISurfaceProvider is
+  /// using. May vary over the lifetime of the ISurfaceProvider. The default
+  /// implementation is appropriate for static ISurfaceProviders.
+  virtual void AddSizeOfExcludingThis(MallocSizeOf aMallocSizeOf,
+                                      size_t& aHeapSizeOut,
+                                      size_t& aNonHeapSizeOut)
+  {
+    DrawableFrameRef ref = DrawableRef(/* aFrame = */ 0);
+    if (!ref) {
+      return;
+    }
+
+    ref->AddSizeOfExcludingThis(aMallocSizeOf, aHeapSizeOut, aNonHeapSizeOut);
+  }
+
   /// @return the availability state of this ISurfaceProvider, which indicates
   /// whether DrawableRef() could successfully return a surface. Should only be
   /// called from SurfaceCache code as it relies on SurfaceCache for
   /// synchronization.
   AvailabilityState& Availability() { return mAvailability; }
   const AvailabilityState& Availability() const { return mAvailability; }
 
 protected:
--- a/image/Image.h
+++ b/image/Image.h
@@ -70,26 +70,23 @@ struct SurfaceMemoryCounter
                        SurfaceMemoryCounterType aType =
                          SurfaceMemoryCounterType::NORMAL)
     : mKey(aKey)
     , mType(aType)
     , mIsLocked(aIsLocked)
   { }
 
   const SurfaceKey& Key() const { return mKey; }
-  Maybe<gfx::IntSize>& SubframeSize() { return mSubframeSize; }
-  const Maybe<gfx::IntSize>& SubframeSize() const { return mSubframeSize; }
   MemoryCounter& Values() { return mValues; }
   const MemoryCounter& Values() const { return mValues; }
   SurfaceMemoryCounterType Type() const { return mType; }
   bool IsLocked() const { return mIsLocked; }
 
 private:
   const SurfaceKey mKey;
-  Maybe<gfx::IntSize> mSubframeSize;
   MemoryCounter mValues;
   const SurfaceMemoryCounterType mType;
   const bool mIsLocked;
 };
 
 struct ImageMemoryCounter
 {
   ImageMemoryCounter(Image* aImage,
new file mode 100644
--- /dev/null
+++ b/image/PlaybackType.h
@@ -0,0 +1,44 @@
+/* -*- 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/. */
+
+#ifndef mozilla_image_PlaybackType_h
+#define mozilla_image_PlaybackType_h
+
+#include "imgIContainer.h"
+
+namespace mozilla {
+namespace image {
+
+/**
+ * PlaybackType identifies a surface cache entry as either a static surface or
+ * an animation. Note that a specific cache entry is one or the other, but
+ * images may be associated with both types of cache entries, since in some
+ * circumstances we may want to treat an animated image as if it were static.
+ */
+enum class PlaybackType : uint8_t
+{
+  eStatic,   // Calls to DrawableRef() will always return the same surface.
+  eAnimated  // An animation; calls to DrawableRef() may return different
+             // surfaces at different times.
+};
+
+/**
+ * Given an imgIContainer FRAME_* value, returns the corresponding PlaybackType
+ * for use in surface cache lookups.
+ */
+inline PlaybackType
+ToPlaybackType(uint32_t aWhichFrame)
+{
+  MOZ_ASSERT(aWhichFrame == imgIContainer::FRAME_FIRST ||
+             aWhichFrame == imgIContainer::FRAME_CURRENT);
+  return aWhichFrame == imgIContainer::FRAME_CURRENT
+       ? PlaybackType::eAnimated
+       : PlaybackType::eStatic;
+}
+
+} // namespace image
+} // namespace mozilla
+
+#endif // mozilla_image_PlaybackType_h
--- a/image/RasterImage.cpp
+++ b/image/RasterImage.cpp
@@ -265,92 +265,89 @@ RasterImage::GetType(uint16_t* aType)
 {
   NS_ENSURE_ARG_POINTER(aType);
 
   *aType = imgIContainer::TYPE_RASTER;
   return NS_OK;
 }
 
 LookupResult
-RasterImage::LookupFrameInternal(uint32_t aFrameNum,
-                                 const IntSize& aSize,
-                                 uint32_t aFlags)
+RasterImage::LookupFrameInternal(const IntSize& aSize,
+                                 uint32_t aFlags,
+                                 PlaybackType aPlaybackType)
 {
-  if (!mAnimationState) {
-    NS_ASSERTION(aFrameNum == 0,
-                 "Don't ask for a frame > 0 if we're not animated!");
-    aFrameNum = 0;
-  }
-
-  if (mAnimationState && aFrameNum > 0) {
+  if (mAnimationState && aPlaybackType == PlaybackType::eAnimated) {
     MOZ_ASSERT(mFrameAnimator);
     MOZ_ASSERT(ToSurfaceFlags(aFlags) == DefaultSurfaceFlags(),
                "Can't composite frames with non-default surface flags");
-    return mFrameAnimator->GetCompositedFrame(aFrameNum);
+    const size_t index = mAnimationState->GetCurrentAnimationFrameIndex();
+    return mFrameAnimator->GetCompositedFrame(index);
   }
 
   SurfaceFlags surfaceFlags = ToSurfaceFlags(aFlags);
 
   // We don't want any substitution for sync decodes, and substitution would be
   // illegal when high quality downscaling is disabled, so we use
   // SurfaceCache::Lookup in this case.
   if ((aFlags & FLAG_SYNC_DECODE) || !(aFlags & FLAG_HIGH_QUALITY_SCALING)) {
     return SurfaceCache::Lookup(ImageKey(this),
                                 RasterSurfaceKey(aSize,
                                                  surfaceFlags,
-                                                 aFrameNum));
+                                                 PlaybackType::eStatic));
   }
 
   // We'll return the best match we can find to the requested frame.
   return SurfaceCache::LookupBestMatch(ImageKey(this),
                                        RasterSurfaceKey(aSize,
                                                         surfaceFlags,
-                                                        aFrameNum));
+                                                        PlaybackType::eStatic));
 }
 
 DrawableSurface
-RasterImage::LookupFrame(uint32_t aFrameNum,
-                         const IntSize& aSize,
-                         uint32_t aFlags)
+RasterImage::LookupFrame(const IntSize& aSize,
+                         uint32_t aFlags,
+                         PlaybackType aPlaybackType)
 {
   MOZ_ASSERT(NS_IsMainThread());
 
   // If we're opaque, we don't need to care about premultiplied alpha, because
   // that can only matter for frames with transparency.
   if (IsOpaque()) {
     aFlags &= ~FLAG_DECODE_NO_PREMULTIPLY_ALPHA;
   }
 
   IntSize requestedSize = CanDownscaleDuringDecode(aSize, aFlags)
                         ? aSize : mSize;
   if (requestedSize.IsEmpty()) {
     return DrawableSurface();  // Can't decode to a surface of zero size.
   }
 
-  LookupResult result = LookupFrameInternal(aFrameNum, requestedSize, aFlags);
+  LookupResult result =
+    LookupFrameInternal(requestedSize, aFlags, aPlaybackType);
 
   if (!result && !mHasSize) {
     // We can't request a decode without knowing our intrinsic size. Give up.
     return DrawableSurface();
   }
 
   if (result.Type() == MatchType::NOT_FOUND ||
       result.Type() == MatchType::SUBSTITUTE_BECAUSE_NOT_FOUND ||
       ((aFlags & FLAG_SYNC_DECODE) && !result)) {
     // We don't have a copy of this frame, and there's no decoder working on
     // one. (Or we're sync decoding and the existing decoder hasn't even started
     // yet.) Trigger decoding so it'll be available next time.
-    MOZ_ASSERT(!mAnimationState || mAnimationState->KnownFrameCount() < 1,
+    MOZ_ASSERT(aPlaybackType != PlaybackType::eAnimated ||
+               !mAnimationState || mAnimationState->KnownFrameCount() < 1,
                "Animated frames should be locked");
 
-    Decode(requestedSize, aFlags);
+    Decode(requestedSize, aFlags, aPlaybackType);
 
     // If we can sync decode, we should already have the frame.
     if (aFlags & FLAG_SYNC_DECODE) {
-      result = LookupFrameInternal(aFrameNum, requestedSize, aFlags);
+      result = LookupFrameInternal(requestedSize, aFlags, aPlaybackType);
     }
   }
 
   if (!result) {
     // We still weren't able to get a frame. Give up.
     return DrawableSurface();
   }
 
@@ -375,32 +372,16 @@ RasterImage::LookupFrame(uint32_t aFrame
   if (aFlags & (FLAG_SYNC_DECODE | FLAG_SYNC_DECODE_IF_FAST) &&
     result.Surface()->IsAborted()) {
     return DrawableSurface();
   }
 
   return Move(result.Surface());
 }
 
-uint32_t
-RasterImage::GetCurrentFrameIndex() const
-{
-  if (mAnimationState) {
-    return mAnimationState->GetCurrentAnimationFrameIndex();
-  }
-
-  return 0;
-}
-
-uint32_t
-RasterImage::GetRequestedFrameIndex(uint32_t aWhichFrame) const
-{
-  return aWhichFrame == FRAME_FIRST ? 0 : GetCurrentFrameIndex();
-}
-
 NS_IMETHODIMP_(bool)
 RasterImage::IsOpaque()
 {
   if (mError) {
     return false;
   }
 
   Progress progress = mProgressTracker->GetProgress();
@@ -501,19 +482,19 @@ RasterImage::GetFrameInternal(const IntS
   }
 
   if (mError) {
     return MakePair(DrawResult::BAD_IMAGE, RefPtr<SourceSurface>());
   }
 
   // Get the frame. If it's not there, it's probably the caller's fault for
   // not waiting for the data to be loaded from the network or not passing
-  // FLAG_SYNC_DECODE
+  // FLAG_SYNC_DECODE.
   DrawableSurface surface =
-    LookupFrame(GetRequestedFrameIndex(aWhichFrame), aSize, aFlags);
+    LookupFrame(aSize, aFlags, ToPlaybackType(aWhichFrame));
   if (!surface) {
     // The OS threw this frame away and we couldn't redecode it.
     return MakePair(DrawResult::TEMPORARY_ERROR, RefPtr<SourceSurface>());
   }
 
   RefPtr<SourceSurface> sourceSurface = surface->GetSourceSurface();
 
   if (!surface->IsFinished()) {
@@ -761,17 +742,17 @@ RasterImage::StartAnimation()
   // If we're not ready to animate, then set mPendingAnimation, which will cause
   // us to start animating if and when we do become ready.
   mPendingAnimation = !mAnimationState || mAnimationState->KnownFrameCount() < 1;
   if (mPendingAnimation) {
     return NS_OK;
   }
 
   // Don't bother to animate if we're displaying the first frame forever.
-  if (GetCurrentFrameIndex() == 0 &&
+  if (mAnimationState->GetCurrentAnimationFrameIndex() == 0 &&
       mAnimationState->FirstFrameTimeout() == FrameTimeout::Forever()) {
     mAnimationFinished = true;
     return NS_ERROR_ABORT;
   }
 
   // We need to set the time that this initial frame was first displayed, as
   // this is used in AdvanceFrame().
   mAnimationState->InitAnimationFrameTimeIfNecessary();
@@ -1057,19 +1038,19 @@ RasterImage::RequestDecodeForSize(const 
   // redecoding an image (see bug 845147).
   bool shouldSyncDecodeIfFast =
     !mHasBeenDecoded && (aFlags & FLAG_SYNC_DECODE_IF_FAST);
 
   uint32_t flags = shouldSyncDecodeIfFast
                  ? aFlags
                  : aFlags & ~FLAG_SYNC_DECODE_IF_FAST;
 
-  // Look up the first frame of the image, which will implicitly start decoding
-  // if it's not available right now.
-  LookupFrame(0, aSize, flags);
+  // Perform a frame lookup, which will implicitly start decoding if needed.
+  LookupFrame(aSize, flags, mAnimationState ? PlaybackType::eAnimated
+                                            : PlaybackType::eStatic);
 
   return NS_OK;
 }
 
 static void
 LaunchDecodingTask(IDecodingTask* aTask,
                    RasterImage* aImage,
                    uint32_t aFlags,
@@ -1095,17 +1076,19 @@ LaunchDecodingTask(IDecodingTask* aTask,
   }
 
   // Perform an async decode. We also take this path if we don't have all the
   // source data yet, since sync decoding is impossible in that situation.
   DecodePool::Singleton()->AsyncRun(aTask);
 }
 
 NS_IMETHODIMP
-RasterImage::Decode(const IntSize& aSize, uint32_t aFlags)
+RasterImage::Decode(const IntSize& aSize,
+                    uint32_t aFlags,
+                    PlaybackType aPlaybackType)
 {
   MOZ_ASSERT(NS_IsMainThread());
 
   if (mError) {
     return NS_ERROR_FAILURE;
   }
 
   // If we don't have a size yet, we can't do any other decoding.
@@ -1139,17 +1122,17 @@ RasterImage::Decode(const IntSize& aSize
   if (IsOpaque()) {
     // If there's no transparency, it doesn't matter whether we premultiply
     // alpha or not.
     surfaceFlags &= ~SurfaceFlags::NO_PREMULTIPLY_ALPHA;
   }
 
   // Create a decoder.
   RefPtr<IDecodingTask> task;
-  if (mAnimationState) {
+  if (mAnimationState && aPlaybackType == PlaybackType::eAnimated) {
     task = DecoderFactory::CreateAnimationDecoder(mDecoderType, WrapNotNull(this),
                                                   mSourceBuffer, mSize,
                                                   decoderFlags, surfaceFlags);
   } else {
     task = DecoderFactory::CreateDecoder(mDecoderType, WrapNotNull(this),
                                          mSourceBuffer, mSize, aSize,
                                          decoderFlags, surfaceFlags,
                                          mRequestedSampleSize);
@@ -1206,23 +1189,23 @@ RasterImage::RecoverFromInvalidFrames(co
   // Relock the image if it's supposed to be locked.
   if (mLockCount > 0) {
     SurfaceCache::LockImage(ImageKey(this));
   }
 
   // Animated images require some special handling, because we normally require
   // that they never be discarded.
   if (mAnimationState) {
-    Decode(mSize, aFlags | FLAG_SYNC_DECODE);
+    Decode(mSize, aFlags | FLAG_SYNC_DECODE, PlaybackType::eAnimated);
     ResetAnimation();
     return;
   }
 
   // For non-animated images, it's fine to recover using an async decode.
-  Decode(aSize, aFlags);
+  Decode(aSize, aFlags, PlaybackType::eStatic);
 }
 
 static bool
 HaveSkia()
 {
 #ifdef MOZ_ENABLE_SKIA
   return true;
 #else
@@ -1338,17 +1321,17 @@ RasterImage::Draw(gfxContext* aContext,
 
   // If we're not using SamplingFilter::GOOD, we shouldn't high-quality scale or
   // downscale during decode.
   uint32_t flags = aSamplingFilter == SamplingFilter::GOOD
                  ? aFlags
                  : aFlags & ~FLAG_HIGH_QUALITY_SCALING;
 
   DrawableSurface surface =
-    LookupFrame(GetRequestedFrameIndex(aWhichFrame), aSize, flags);
+    LookupFrame(aSize, flags, ToPlaybackType(aWhichFrame));
   if (!surface) {
     // Getting the frame (above) touches the image and kicks off decoding.
     if (mDrawStartTime.IsNull()) {
       mDrawStartTime = TimeStamp::Now();
     }
     return DrawResult::NOT_READY;
   }
 
@@ -1514,54 +1497,59 @@ RasterImage::GetFramesNotified(uint32_t*
   return NS_OK;
 }
 #endif
 
 void
 RasterImage::NotifyProgress(Progress aProgress,
                             const IntRect& aInvalidRect /* = IntRect() */,
                             const Maybe<uint32_t>& aFrameCount /* = Nothing() */,
+                            DecoderFlags aDecoderFlags
+                              /* = DefaultDecoderFlags() */,
                             SurfaceFlags aSurfaceFlags
                               /* = DefaultSurfaceFlags() */)
 {
   MOZ_ASSERT(NS_IsMainThread());
 
   // Ensure that we stay alive long enough to finish notifying.
   RefPtr<RasterImage> image = this;
 
   const bool wasDefaultFlags = aSurfaceFlags == DefaultSurfaceFlags();
 
   if (!aInvalidRect.IsEmpty() && wasDefaultFlags) {
     // Update our image container since we're invalidating.
     UpdateImageContainer();
   }
 
-  // We may have decoded new animation frames; update our animation state.
-  MOZ_ASSERT_IF(aFrameCount && *aFrameCount > 1, mAnimationState || mError);
-  if (mAnimationState && aFrameCount) {
-    mAnimationState->UpdateKnownFrameCount(*aFrameCount);
-  }
+  if (!(aDecoderFlags & DecoderFlags::FIRST_FRAME_ONLY)) {
+    // We may have decoded new animation frames; update our animation state.
+    MOZ_ASSERT_IF(aFrameCount && *aFrameCount > 1, mAnimationState || mError);
+    if (mAnimationState && aFrameCount) {
+      mAnimationState->UpdateKnownFrameCount(*aFrameCount);
+    }
 
-  // If we should start animating right now, do so.
-  if (mAnimationState && aFrameCount == Some(1u) &&
-      mPendingAnimation && ShouldAnimate()) {
-    StartAnimation();
+    // If we should start animating right now, do so.
+    if (mAnimationState && aFrameCount == Some(1u) &&
+        mPendingAnimation && ShouldAnimate()) {
+      StartAnimation();
+    }
   }
 
   // Tell the observers what happened.
   image->mProgressTracker->SyncNotifyProgress(aProgress, aInvalidRect);
 }
 
 void
 RasterImage::NotifyDecodeComplete(const DecoderFinalStatus& aStatus,
                                   const ImageMetadata& aMetadata,
                                   const DecoderTelemetry& aTelemetry,
                                   Progress aProgress,
                                   const IntRect& aInvalidRect,
                                   const Maybe<uint32_t>& aFrameCount,
+                                  DecoderFlags aDecoderFlags,
                                   SurfaceFlags aSurfaceFlags)
 {
   MOZ_ASSERT(NS_IsMainThread());
 
   // If the decoder detected an error, log it to the error console.
   if (aStatus.mShouldReportError && !aStatus.mWasAborted) {
     ReportDecoderError();
   }
@@ -1581,21 +1569,23 @@ RasterImage::NotifyDecodeComplete(const 
              "SetMetadata should've gotten a size");
 
   if (!aStatus.mWasMetadataDecode && aStatus.mFinished && !aStatus.mWasAborted) {
     // Flag that we've been decoded before.
     mHasBeenDecoded = true;
   }
 
   // Send out any final notifications.
-  NotifyProgress(aProgress, aInvalidRect, aFrameCount, aSurfaceFlags);
+  NotifyProgress(aProgress, aInvalidRect, aFrameCount,
+                 aDecoderFlags, aSurfaceFlags);
 
-  if (mHasBeenDecoded && mAnimationState) {
-    // We're done decoding and our AnimationState has been notified about all
-    // our frames, so let it know not to expect anymore.
+  if (!(aDecoderFlags & DecoderFlags::FIRST_FRAME_ONLY) &&
+      mHasBeenDecoded && mAnimationState) {
+    // We've finished a full decode of all animation frames and our AnimationState
+    // has been notified about them all, so let it know not to expect anymore.
     mAnimationState->SetDoneDecoding(true);
   }
 
   if (!aStatus.mWasMetadataDecode && aTelemetry.mChunkCount) {
     Telemetry::Accumulate(Telemetry::IMAGE_DECODE_CHUNKS, aTelemetry.mChunkCount);
   }
 
   if (aStatus.mFinished) {
--- a/image/RasterImage.h
+++ b/image/RasterImage.h
@@ -35,16 +35,17 @@
 #include "mozilla/Maybe.h"
 #include "mozilla/MemoryReporting.h"
 #include "mozilla/NotNull.h"
 #include "mozilla/Pair.h"
 #include "mozilla/TimeStamp.h"
 #include "mozilla/WeakPtr.h"
 #include "mozilla/UniquePtr.h"
 #include "ImageContainer.h"
+#include "PlaybackType.h"
 #ifdef DEBUG
   #include "imgIContainerDebug.h"
 #endif
 
 class nsIInputStream;
 class nsIRequest;
 
 #define NS_RASTERIMAGE_CID \
@@ -184,49 +185,55 @@ public:
    * Main-thread only.
    *
    * @param aProgress    The progress notifications to send.
    * @param aInvalidRect An invalidation rect to send.
    * @param aFrameCount  If Some(), an updated count of the number of frames of
    *                     animation the decoder has finished decoding so far. This
    *                     is a lower bound for the total number of animation
    *                     frames this image has.
-   * @param aFlags       The surface flags used by the decoder that generated
-   *                     these notifications, or DefaultSurfaceFlags() if the
-   *                     notifications don't come from a decoder.
+   * @param aDecoderFlags The decoder flags used by the decoder that generated
+   *                      these notifications, or DefaultDecoderFlags() if the
+   *                      notifications don't come from a decoder.
+   * @param aSurfaceFlags The surface flags used by the decoder that generated
+   *                      these notifications, or DefaultSurfaceFlags() if the
+   *                      notifications don't come from a decoder.
    */
   void NotifyProgress(Progress aProgress,
                       const gfx::IntRect& aInvalidRect = nsIntRect(),
                       const Maybe<uint32_t>& aFrameCount = Nothing(),
+                      DecoderFlags aDecoderFlags = DefaultDecoderFlags(),
                       SurfaceFlags aSurfaceFlags = DefaultSurfaceFlags());
 
   /**
    * Records decoding results, sends out any final notifications, updates the
    * state of this image, and records telemetry.
    *
    * Main-thread only.
    *
-   * @param aStatus      Final status information about the decoder. (Whether it
-   *                     encountered an error, etc.)
-   * @param aMetadata    Metadata about this image that the decoder gathered.
-   * @param aTelemetry   Telemetry data about the decoder.
-   * @param aProgress    Any final progress notifications to send.
-   * @param aInvalidRect Any final invalidation rect to send.
-   * @param aFrameCount  If Some(), a final updated count of the number of frames
-   *                     of animation the decoder has finished decoding so far.
-   *                     This is a lower bound for the total number of animation
-   *                     frames this image has.
-   * @param aFlags       The surface flags used by the decoder.
+   * @param aStatus       Final status information about the decoder. (Whether it
+   *                      encountered an error, etc.)
+   * @param aMetadata     Metadata about this image that the decoder gathered.
+   * @param aTelemetry    Telemetry data about the decoder.
+   * @param aProgress     Any final progress notifications to send.
+   * @param aInvalidRect  Any final invalidation rect to send.
+   * @param aFrameCount   If Some(), a final updated count of the number of frames
+   *                      of animation the decoder has finished decoding so far.
+   *                      This is a lower bound for the total number of animation
+   *                      frames this image has.
+   * @param aDecoderFlags The decoder flags used by the decoder.
+   * @param aSurfaceFlags The surface flags used by the decoder.
    */
   void NotifyDecodeComplete(const DecoderFinalStatus& aStatus,
                             const ImageMetadata& aMetadata,
                             const DecoderTelemetry& aTelemetry,
                             Progress aProgress,
                             const gfx::IntRect& aInvalidRect,
                             const Maybe<uint32_t>& aFrameCount,
+                            DecoderFlags aDecoderFlags,
                             SurfaceFlags aSurfaceFlags);
 
   // Helper method for NotifyDecodeComplete.
   void ReportDecoderError();
 
 
   //////////////////////////////////////////////////////////////////////////////
   // Network callbacks.
@@ -268,37 +275,50 @@ public:
       GetURI()->GetSpec(spec);
     }
     return spec;
   }
 
 private:
   nsresult Init(const char* aMimeType, uint32_t aFlags);
 
-  DrawResult DrawInternal(DrawableSurface&& aSurface,
+  /**
+   * Tries to retrieve a surface for this image with size @aSize, surface flags
+   * matching @aFlags, and a playback type of @aPlaybackType.
+   *
+   * If @aFlags specifies FLAG_SYNC_DECODE and we already have all the image
+   * data, we'll attempt a sync decode if no matching surface is found. If
+   * FLAG_SYNC_DECODE was not specified and no matching surface was found, we'll
+   * kick off an async decode so that the surface is (hopefully) available next
+   * time it's requested.
+   *
+   * @return a drawable surface, which may be empty if the requested surface
+   *         could not be found.
+   */
+  DrawableSurface LookupFrame(const gfx::IntSize& aSize,
+                              uint32_t aFlags,
+                              PlaybackType aPlaybackType);
+
+  /// Helper method for LookupFrame().
+  LookupResult LookupFrameInternal(const gfx::IntSize& aSize,
+                                   uint32_t aFlags,
+                                   PlaybackType aPlaybackType);
+
+  DrawResult DrawInternal(DrawableSurface&& aFrameRef,
                           gfxContext* aContext,
                           const nsIntSize& aSize,
                           const ImageRegion& aRegion,
                           gfx::SamplingFilter aSamplingFilter,
                           uint32_t aFlags);
 
   Pair<DrawResult, RefPtr<gfx::SourceSurface>>
     GetFrameInternal(const gfx::IntSize& aSize,
                      uint32_t aWhichFrame,
                      uint32_t aFlags);
 
-  LookupResult LookupFrameInternal(uint32_t aFrameNum,
-                                   const gfx::IntSize& aSize,
-                                   uint32_t aFlags);
-  DrawableSurface LookupFrame(uint32_t aFrameNum,
-                              const nsIntSize& aSize,
-                              uint32_t aFlags);
-  uint32_t GetCurrentFrameIndex() const;
-  uint32_t GetRequestedFrameIndex(uint32_t aWhichFrame) const;
-
   Pair<DrawResult, RefPtr<layers::Image>>
     GetCurrentImage(layers::ImageContainer* aContainer, uint32_t aFlags);
 
   void UpdateImageContainer();
 
   // We would like to just check if we have a zero lock count, but we can't do
   // that for animated images because in EnsureAnimExists we lock the image and
   // never unlock so that animated images always have their lock count >= 1. In
@@ -310,22 +330,26 @@ private:
 
   //////////////////////////////////////////////////////////////////////////////
   // Decoding.
   //////////////////////////////////////////////////////////////////////////////
 
   /**
    * Creates and runs a decoder, either synchronously or asynchronously
    * according to @aFlags. Decodes at the provided target size @aSize, using
-   * decode flags @aFlags.
+   * decode flags @aFlags. Performs a single-frame decode of this image unless
+   * we know the image is animated *and* @aPlaybackType is
+   * PlaybackType::eAnimated.
    *
    * It's an error to call Decode() before this image's intrinsic size is
    * available. A metadata decode must successfully complete first.
    */
-  NS_IMETHOD Decode(const gfx::IntSize& aSize, uint32_t aFlags);
+  NS_IMETHOD Decode(const gfx::IntSize& aSize,
+                    uint32_t aFlags,
+                    PlaybackType aPlaybackType);
 
   /**
    * Creates and runs a metadata decoder, either synchronously or
    * asynchronously according to @aFlags.
    */
   NS_IMETHOD DecodeMetadata(uint32_t aFlags);
 
   /**
--- a/image/SurfaceCache.cpp
+++ b/image/SurfaceCache.cpp
@@ -193,27 +193,30 @@ public:
 
     void Add(CachedSurface* aCachedSurface)
     {
       MOZ_ASSERT(aCachedSurface, "Should have a CachedSurface");
 
       SurfaceMemoryCounter counter(aCachedSurface->GetSurfaceKey(),
                                    aCachedSurface->IsLocked());
 
-      if (!aCachedSurface->IsPlaceholder()) {
-        DrawableSurface drawableSurface = aCachedSurface->GetDrawableSurface();
-        if (drawableSurface) {
-          counter.SubframeSize() = Some(drawableSurface->GetSize());
+      if (aCachedSurface->IsPlaceholder()) {
+        return;
+      }
 
-          size_t heap = 0, nonHeap = 0;
-          drawableSurface->AddSizeOfExcludingThis(mMallocSizeOf, heap, nonHeap);
-          counter.Values().SetDecodedHeap(heap);
-          counter.Values().SetDecodedNonHeap(nonHeap);
-        }
-      }
+      // 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.)
+      size_t heap = 0;
+      size_t nonHeap = 0;
+      aCachedSurface->mProvider
+        ->AddSizeOfExcludingThis(mMallocSizeOf, heap, nonHeap);
+      counter.Values().SetDecodedHeap(heap);
+      counter.Values().SetDecodedNonHeap(nonHeap);
 
       mCounters.AppendElement(counter);
     }
 
   private:
     nsTArray<SurfaceMemoryCounter>& mCounters;
     MallocSizeOf                    mMallocSizeOf;
   };
@@ -293,18 +296,18 @@ public:
     for (auto iter = ConstIter(); !iter.Done(); iter.Next()) {
       CachedSurface* current = iter.UserData();
       const SurfaceKey& currentKey = current->GetSurfaceKey();
 
       // We never match a placeholder.
       if (current->IsPlaceholder()) {
         continue;
       }
-      // Matching the animation time and SVG context is required.
-      if (currentKey.AnimationTime() != aIdealKey.AnimationTime() ||
+      // 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
@@ -650,17 +653,17 @@ public:
       Remove(surface);
     }
 
     MOZ_ASSERT_IF(matchType == MatchType::EXACT,
                   surface->GetSurfaceKey() == aSurfaceKey);
     MOZ_ASSERT_IF(matchType == MatchType::SUBSTITUTE_BECAUSE_NOT_FOUND ||
                   matchType == MatchType::SUBSTITUTE_BECAUSE_PENDING,
       surface->GetSurfaceKey().SVGContext() == aSurfaceKey.SVGContext() &&
-      surface->GetSurfaceKey().AnimationTime() == aSurfaceKey.AnimationTime() &&
+      surface->GetSurfaceKey().Playback() == aSurfaceKey.Playback() &&
       surface->GetSurfaceKey().Flags() == aSurfaceKey.Flags());
 
     if (matchType == MatchType::EXACT) {
       MarkUsed(surface, cache);
     }
 
     return LookupResult(Move(drawableSurface), matchType);
   }
--- a/image/SurfaceCache.h
+++ b/image/SurfaceCache.h
@@ -15,16 +15,17 @@
 #include "mozilla/NotNull.h"
 #include "mozilla/MemoryReporting.h" // for MallocSizeOf
 #include "mozilla/HashFunctions.h"   // for HashGeneric and AddToHash
 #include "gfx2DGlue.h"
 #include "gfxPoint.h"                // for gfxSize
 #include "nsCOMPtr.h"                // for already_AddRefed
 #include "mozilla/gfx/Point.h"       // for mozilla::gfx::IntSize
 #include "mozilla/gfx/2D.h"          // for SourceSurface
+#include "PlaybackType.h"
 #include "SurfaceFlags.h"
 #include "SVGImageContext.h"         // for SVGImageContext
 
 namespace mozilla {
 namespace image {
 
 class Image;
 class ISurfaceProvider;
@@ -50,78 +51,77 @@ class SurfaceKey
 {
   typedef gfx::IntSize IntSize;
 
 public:
   bool operator==(const SurfaceKey& aOther) const
   {
     return aOther.mSize == mSize &&
            aOther.mSVGContext == mSVGContext &&
-           aOther.mAnimationTime == mAnimationTime &&
+           aOther.mPlayback == mPlayback &&
            aOther.mFlags == mFlags;
   }
 
   uint32_t Hash() const
   {
     uint32_t hash = HashGeneric(mSize.width, mSize.height);
     hash = AddToHash(hash, mSVGContext.map(HashSIC).valueOr(0));
-    hash = AddToHash(hash, mAnimationTime, uint32_t(mFlags));
+    hash = AddToHash(hash, uint8_t(mPlayback), uint32_t(mFlags));
     return hash;
   }
 
   const IntSize& Size() const { return mSize; }
   Maybe<SVGImageContext> SVGContext() const { return mSVGContext; }
-  float AnimationTime() const { return mAnimationTime; }
+  PlaybackType Playback() const { return mPlayback; }
   SurfaceFlags Flags() const { return mFlags; }
 
 private:
   SurfaceKey(const IntSize& aSize,
              const Maybe<SVGImageContext>& aSVGContext,
-             const float aAnimationTime,
-             const SurfaceFlags aFlags)
+             PlaybackType aPlayback,
+             SurfaceFlags aFlags)
     : mSize(aSize)
     , mSVGContext(aSVGContext)
-    , mAnimationTime(aAnimationTime)
+    , mPlayback(aPlayback)
     , mFlags(aFlags)
   { }
 
   static uint32_t HashSIC(const SVGImageContext& aSIC) {
     return aSIC.Hash();
   }
 
-  friend SurfaceKey RasterSurfaceKey(const IntSize&,
-                                     SurfaceFlags,
-                                     uint32_t);
+  friend SurfaceKey RasterSurfaceKey(const IntSize&, SurfaceFlags, PlaybackType);
   friend SurfaceKey VectorSurfaceKey(const IntSize&,
-                                     const Maybe<SVGImageContext>&,
-                                     float);
+                                     const Maybe<SVGImageContext>&);
 
   IntSize                mSize;
   Maybe<SVGImageContext> mSVGContext;
-  float                  mAnimationTime;
+  PlaybackType           mPlayback;
   SurfaceFlags           mFlags;
 };
 
 inline SurfaceKey
 RasterSurfaceKey(const gfx::IntSize& aSize,
                  SurfaceFlags aFlags,
-                 uint32_t aFrameNum)
+                 PlaybackType aPlayback)
 {
-  return SurfaceKey(aSize, Nothing(), float(aFrameNum), aFlags);
+  return SurfaceKey(aSize, Nothing(), aPlayback, aFlags);
 }
 
 inline SurfaceKey
 VectorSurfaceKey(const gfx::IntSize& aSize,
-                 const Maybe<SVGImageContext>& aSVGContext,
-                 float aAnimationTime)
+                 const Maybe<SVGImageContext>& aSVGContext)
 {
   // We don't care about aFlags for VectorImage because none of the flags we
   // have right now influence VectorImage's rendering. If we add a new flag that
   // *does* affect how a VectorImage renders, we'll have to change this.
-  return SurfaceKey(aSize, aSVGContext, aAnimationTime, DefaultSurfaceFlags());
+  // Similarly, we don't accept a PlaybackType parameter because we don't
+  // currently cache frames of animated SVG images.
+  return SurfaceKey(aSize, aSVGContext, PlaybackType::eStatic,
+                    DefaultSurfaceFlags());
 }
 
 
 /**
  * AvailabilityState is used to track whether an ISurfaceProvider has a surface
  * available or is just a placeholder.
  *
  * To ensure that availability changes are atomic (and especially that internal
--- a/image/VectorImage.cpp
+++ b/image/VectorImage.cpp
@@ -875,21 +875,25 @@ VectorImage::Draw(gfxContext* aContext,
 already_AddRefed<gfxDrawable>
 VectorImage::LookupCachedSurface(const SVGDrawingParameters& aParams)
 {
   // If we're not allowed to use a cached surface, don't attempt a lookup.
   if (aParams.flags & FLAG_BYPASS_SURFACE_CACHE) {
     return nullptr;
   }
 
+  // We don't do any caching if we have animation, so don't bother with a lookup
+  // in this case either.
+  if (mHaveAnimations) {
+    return nullptr;
+  }
+
   LookupResult result =
     SurfaceCache::Lookup(ImageKey(this),
-                         VectorSurfaceKey(aParams.size,
-                                          aParams.svgContext,
-                                          aParams.animationTime));
+                         VectorSurfaceKey(aParams.size, aParams.svgContext));
   if (!result) {
     return nullptr;  // No matching surface, or the OS freed the volatile buffer.
   }
 
   RefPtr<SourceSurface> sourceSurface = result.Surface()->GetSourceSurface();
   if (!sourceSurface) {
     // Something went wrong. (Probably a GPU driver crash or device reset.)
     // Attempt to recover.
@@ -956,19 +960,17 @@ VectorImage::CreateSurfaceAndShow(const 
   if (!surface) {
     return Show(svgDrawable, aParams);
   }
 
   // Attempt to cache the frame.
   NotNull<RefPtr<ISurfaceProvider>> provider =
     WrapNotNull(new SimpleSurfaceProvider(frame));
   SurfaceCache::Insert(provider, ImageKey(this),
-                       VectorSurfaceKey(aParams.size,
-                                        aParams.svgContext,
-                                        aParams.animationTime));
+                       VectorSurfaceKey(aParams.size, aParams.svgContext));
 
   // Draw.
   RefPtr<gfxDrawable> drawable =
     new gfxSurfaceDrawable(surface, aParams.size);
   Show(drawable, aParams);
 
   // Send out an invalidation so that surfaces that are still in use get
   // re-locked. See the discussion of the UnlockSurfaces call above.
--- a/image/imgLoader.cpp
+++ b/image/imgLoader.cpp
@@ -287,32 +287,25 @@ private:
                                  nsISupports* aData,
                                  const nsACString& aPathPrefix,
                                  const ImageMemoryCounter& aCounter)
   {
     for (const SurfaceMemoryCounter& counter : aCounter.Surfaces()) {
       nsAutoCString surfacePathPrefix(aPathPrefix);
       surfacePathPrefix.Append(counter.IsLocked() ? "locked/" : "unlocked/");
       surfacePathPrefix.Append("surface(");
-
-      if (counter.SubframeSize() &&
-          *counter.SubframeSize() != counter.Key().Size()) {
-        surfacePathPrefix.AppendInt(counter.SubframeSize()->width);
-        surfacePathPrefix.Append("x");
-        surfacePathPrefix.AppendInt(counter.SubframeSize()->height);
-        surfacePathPrefix.Append(" subframe of ");
-      }
-
       surfacePathPrefix.AppendInt(counter.Key().Size().width);
       surfacePathPrefix.Append("x");
       surfacePathPrefix.AppendInt(counter.Key().Size().height);
 
       if (counter.Type() == SurfaceMemoryCounterType::NORMAL) {
-        surfacePathPrefix.Append("@");
-        surfacePathPrefix.AppendFloat(counter.Key().AnimationTime());
+        PlaybackType playback = counter.Key().Playback();
+        surfacePathPrefix.Append(playback == PlaybackType::eAnimated
+                                 ? " (animation)"
+                                 : "");
 
         if (counter.Key().Flags() != DefaultSurfaceFlags()) {
           surfacePathPrefix.Append(", flags:");
           surfacePathPrefix.AppendInt(uint32_t(counter.Key().Flags()),
                                       /* aRadix = */ 16);
         }
       } else if (counter.Type() == SurfaceMemoryCounterType::COMPOSITING) {
         surfacePathPrefix.Append(", compositing frame");
--- a/image/test/gtest/TestDecoders.cpp
+++ b/image/test/gtest/TestDecoders.cpp
@@ -414,22 +414,21 @@ TEST_F(ImageDecoders, AnimatedGIFWithExt
   EXPECT_EQ(testCase.mSize.height, imageSize.height);
 
   Progress imageProgress = tracker->GetProgress();
 
   EXPECT_TRUE(bool(imageProgress & FLAG_HAS_TRANSPARENCY) == false);
   EXPECT_TRUE(bool(imageProgress & FLAG_IS_ANIMATED) == true);
 
   // Ensure that we decoded both frames of the image.
-  LookupResult firstFrameLookupResult =
+  LookupResult result =
     SurfaceCache::Lookup(ImageKey(image.get()),
                          RasterSurfaceKey(imageSize,
                                           DefaultSurfaceFlags(),
-                                          /* aFrameNum = */ 0));
-  EXPECT_EQ(MatchType::EXACT, firstFrameLookupResult.Type());
+                                          PlaybackType::eAnimated));
+  ASSERT_EQ(MatchType::EXACT, result.Type());
 
-  LookupResult secondFrameLookupResult =
-    SurfaceCache::Lookup(ImageKey(image.get()),
-                         RasterSurfaceKey(imageSize,
-                                          DefaultSurfaceFlags(),
-                                          /* aFrameNum = */ 1));
-  EXPECT_EQ(MatchType::EXACT, secondFrameLookupResult.Type());
+  EXPECT_TRUE(NS_SUCCEEDED(result.Surface().Seek(0)));
+  EXPECT_TRUE(bool(result.Surface()));
+
+  EXPECT_TRUE(NS_SUCCEEDED(result.Surface().Seek(1)));
+  EXPECT_TRUE(bool(result.Surface()));
 }
--- a/image/test/gtest/TestMetadata.cpp
+++ b/image/test/gtest/TestMetadata.cpp
@@ -235,22 +235,21 @@ TEST_F(ImageDecoderMetadata, NoFrameDela
   EXPECT_EQ(testCase.mSize.height, imageSize.height);
 
   Progress imageProgress = tracker->GetProgress();
 
   EXPECT_TRUE(bool(imageProgress & FLAG_HAS_TRANSPARENCY) == false);
   EXPECT_TRUE(bool(imageProgress & FLAG_IS_ANIMATED) == true);
 
   // Ensure that we decoded both frames of the image.
-  LookupResult firstFrameLookupResult =
+  LookupResult result =
     SurfaceCache::Lookup(ImageKey(image.get()),
                          RasterSurfaceKey(imageSize,
                                           DefaultSurfaceFlags(),
-                                          /* aFrameNum = */ 0));
-  EXPECT_EQ(MatchType::EXACT, firstFrameLookupResult.Type());
-                                                             
-  LookupResult secondFrameLookupResult =
-    SurfaceCache::Lookup(ImageKey(image.get()),
-                         RasterSurfaceKey(imageSize,
-                                          DefaultSurfaceFlags(),
-                                          /* aFrameNum = */ 1));
-  EXPECT_EQ(MatchType::EXACT, secondFrameLookupResult.Type());
+                                          PlaybackType::eAnimated));
+  ASSERT_EQ(MatchType::EXACT, result.Type());
+
+  EXPECT_TRUE(NS_SUCCEEDED(result.Surface().Seek(0)));
+  EXPECT_TRUE(bool(result.Surface()));
+
+  EXPECT_TRUE(NS_SUCCEEDED(result.Surface().Seek(1)));
+  EXPECT_TRUE(bool(result.Surface()));
 }
--- a/image/test/mochitest/mochitest.ini
+++ b/image/test/mochitest/mochitest.ini
@@ -142,15 +142,16 @@ skip-if = buildapp == 'b2g' || os == 'an
 skip-if = buildapp == 'b2g' || os == 'android'
 [test_staticClone.html]
 skip-if = buildapp == 'b2g' || os == 'android'
 [test_svg_animatedGIF.html]
 skip-if = buildapp == 'b2g' || os == 'android'
 [test_svg_filter_animation.html]
 skip-if = buildapp == 'b2g' || os == 'android'
 [test_synchronized_animation.html]
-skip-if = buildapp == 'b2g' || os == 'android'
+#skip-if = buildapp == 'b2g' || os == 'android'
+disabled = bug 1295501
 [test_undisplayed_iframe.html]
 skip-if = buildapp == 'b2g' || os == 'android'
 [test_xultree_animation.xhtml]
 skip-if = buildapp == 'b2g' || os == 'android'
 [test_bug1132427.html]
 skip-if = buildapp == 'b2g' || os == 'android'