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 402996 cfb1f1eeceb36952195fd255d5a7a5f017771cbd
parent 402995 02f9e27b988dd63dce023fbca505ac74a1e8a1f6
child 402997 856b1b82372a8273af0c327bb5b6ce5e8e7ff359
push id26791
push userbmo:mh+mozilla@glandium.org
push dateFri, 19 Aug 2016 00:08:51 +0000
reviewersdholbert, edwin, njn
bugs1293472
milestone51.0a1
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'