Bug 1465619 - Part 10. Re-add support for discarding animated image frames. r=tnikkel
authorAndrew Osmond <aosmond@mozilla.com>
Mon, 04 Jun 2018 08:29:50 -0400
changeset 490712 453d21db2a306f4800e8be4d77f6d61000cb9a0c
parent 490711 bd9168e1e48ce09404664e250fdc151d301b98db
child 490713 9c8e6aaa017625dd61942d3100316798c8c2ef61
push id247
push userfmarier@mozilla.com
push dateSat, 27 Oct 2018 01:06:44 +0000
reviewerstnikkel
bugs1465619
milestone65.0a1
Bug 1465619 - Part 10. Re-add support for discarding animated image frames. r=tnikkel AnimatedFrameDiscardingQueue subclasses AnimationFrameBuffer to allow a cleaner abstraction over the behaviour change when we cross the threshold of too high a memory footprint for an animated image. The next patch will build on top of this to provide an abstraction to reuse the discarded frames. Differential Revision: https://phabricator.services.mozilla.com/D7515
image/AnimationFrameBuffer.cpp
image/AnimationFrameBuffer.h
image/AnimationSurfaceProvider.cpp
--- a/image/AnimationFrameBuffer.cpp
+++ b/image/AnimationFrameBuffer.cpp
@@ -149,10 +149,176 @@ AnimationFrameRetainedBuffer::AddSizeOfE
       [&](AddSizeOfCbData& aMetadata) {
         aMetadata.index = i;
         aCallback(aMetadata);
       }
     );
   }
 }
 
+AnimationFrameDiscardingQueue::AnimationFrameDiscardingQueue(AnimationFrameRetainedBuffer&& aQueue)
+  : AnimationFrameBuffer(aQueue)
+  , mInsertIndex(aQueue.mFrames.Length())
+  , mFirstFrame(std::move(aQueue.mFrames[0]))
+{
+  MOZ_ASSERT(!mSizeKnown);
+  MOZ_ASSERT(!mRedecodeError);
+  MOZ_ASSERT(mInsertIndex > 0);
+  MOZ_ASSERT(mGetIndex > 0);
+  mMayDiscard = true;
+
+  for (size_t i = aQueue.mGetIndex; i < mInsertIndex; ++i) {
+    MOZ_ASSERT(aQueue.mFrames[i]);
+    mDisplay.push_back(std::move(aQueue.mFrames[i]));
+  }
+}
+
+bool
+AnimationFrameDiscardingQueue::InsertInternal(RefPtr<imgFrame>&& aFrame)
+{
+  // Even though we don't use redecoded first frames for display purposes, we
+  // will still use them for recycling, so we still need to insert it.
+  mDisplay.push_back(std::move(aFrame));
+  ++mInsertIndex;
+  MOZ_ASSERT(mInsertIndex <= mSize);
+  return true;
+}
+
+bool
+AnimationFrameDiscardingQueue::ResetInternal()
+{
+  mDisplay.clear();
+  mInsertIndex = 0;
+
+  bool restartDecoder = mPending == 0;
+  mPending = 2 * mBatch;
+  return restartDecoder;
+}
+
+bool
+AnimationFrameDiscardingQueue::MarkComplete(const gfx::IntRect& aFirstFrameRefreshArea)
+{
+  if (NS_WARN_IF(mInsertIndex != mSize)) {
+    MOZ_ASSERT(mSizeKnown);
+    mRedecodeError = true;
+    mPending = 0;
+  }
+
+  // We reached the end of the animation, the next frame we get, if we get
+  // another, will be the first frame again.
+  mInsertIndex = 0;
+  mSizeKnown = true;
+
+  // Since we only request advancing when we want to resume at a certain point
+  // in the animation, we should never exceed the number of frames.
+  MOZ_ASSERT(mAdvance == 0);
+  return mPending > 0;
+}
+
+void
+AnimationFrameDiscardingQueue::AdvanceInternal()
+{
+  // We only want to change the current frame index if we have advanced. This
+  // means either a higher frame index, or going back to the beginning.
+  // We should never have advanced beyond the frame buffer.
+  MOZ_ASSERT(mGetIndex < mSize);
+
+  // Unless we are recycling, we should have the current frame still in the
+  // display queue. Either way, we should at least have an entry in the queue
+  // which we need to consume.
+  MOZ_ASSERT(mRecycling || bool(mDisplay.front()));
+  MOZ_ASSERT(!mDisplay.empty());
+  mDisplay.pop_front();
+  MOZ_ASSERT(!mDisplay.empty());
+  MOZ_ASSERT(mDisplay.front());
+
+  if (mDisplay.size() + mPending - 1 < mBatch) {
+    // If we have fewer frames than the batch size, then ask for more. If we
+    // do not have any pending, then we know that there is no active decoding.
+    mPending += mBatch;
+  }
+}
+
+imgFrame*
+AnimationFrameDiscardingQueue::Get(size_t aFrame, bool aForDisplay)
+{
+  // If we are advancing on behalf of the animation, we don't expect it to be
+  // getting any frames (besides the first) until we get the desired frame.
+  MOZ_ASSERT(aFrame == 0 || mAdvance == 0);
+
+  // The first frame is stored separately. If we only need the frame for
+  // display purposes, we can return it right away. If we need it for advancing
+  // the animation, we want to verify the recreated first frame is available
+  // before allowing it continue.
+  if (aForDisplay && aFrame == 0) {
+    return mFirstFrame.get();
+  }
+
+  // If we don't have that frame, return an empty frame ref.
+  if (aFrame >= mSize) {
+    return nullptr;
+  }
+
+  size_t offset;
+  if (aFrame >= mGetIndex) {
+    offset = aFrame - mGetIndex;
+  } else if (!mSizeKnown) {
+    MOZ_ASSERT_UNREACHABLE("Requesting previous frame after we have advanced!");
+    return nullptr;
+  } else {
+    offset = mSize - mGetIndex + aFrame;
+  }
+
+  if (offset >= mDisplay.size()) {
+    return nullptr;
+  }
+
+  // If we have space for the frame, it should always be available.
+  MOZ_ASSERT(mDisplay[offset]);
+  return mDisplay[offset].get();
+}
+
+bool
+AnimationFrameDiscardingQueue::IsFirstFrameFinished() const
+{
+  MOZ_ASSERT(mFirstFrame);
+  MOZ_ASSERT(mFirstFrame->IsFinished());
+  return true;
+}
+
+bool
+AnimationFrameDiscardingQueue::IsLastInsertedFrame(imgFrame* aFrame) const
+{
+  return !mDisplay.empty() && mDisplay.back().get() == aFrame;
+}
+
+void
+AnimationFrameDiscardingQueue::AddSizeOfExcludingThis(MallocSizeOf aMallocSizeOf,
+                                                      const AddSizeOfCb& aCallback)
+{
+  mFirstFrame->AddSizeOfExcludingThis(aMallocSizeOf,
+    [&](AddSizeOfCbData& aMetadata) {
+      aMetadata.index = 1;
+      aCallback(aMetadata);
+    }
+  );
+
+  size_t i = mGetIndex;
+  for (const RefPtr<imgFrame>& frame : mDisplay) {
+    ++i;
+    if (mSize < i) {
+      // First frame again, we already covered it above.
+      MOZ_ASSERT(mFirstFrame.get() == frame.get());
+      i = 1;
+      continue;
+    }
+
+    frame->AddSizeOfExcludingThis(aMallocSizeOf,
+      [&](AddSizeOfCbData& aMetadata) {
+        aMetadata.index = i;
+        aCallback(aMetadata);
+      }
+    );
+  }
+}
+
 } // namespace image
 } // namespace mozilla
--- a/image/AnimationFrameBuffer.h
+++ b/image/AnimationFrameBuffer.h
@@ -2,16 +2,17 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 #ifndef mozilla_image_AnimationFrameBuffer_h
 #define mozilla_image_AnimationFrameBuffer_h
 
 #include "ISurfaceProvider.h"
+#include <deque>
 
 namespace mozilla {
 namespace image {
 
 /**
  * An AnimationFrameBuffer owns the frames outputted by an animated image
  * decoder as well as directing its owner on how to drive the decoder,
  * whether to produce more or to stop.
@@ -179,18 +180,18 @@ public:
 
   /**
    * Inserts a frame into the frame buffer.
    *
    * Once we have a sufficient number of frames buffered relative to the
    * currently displayed frame, it will return YIELD to indicate the caller
    * should stop decoding. Otherwise it will return CONTINUE.
    *
-   * If we cross the threshold, it will return DISCARD or DISCARD_CONTINUE, to
-   * indicate that the caller should switch to a new queue type.
+   * If we cross the threshold, it will return DISCARD_YIELD or DISCARD_CONTINUE
+   * to indicate that the caller should switch to a new queue type.
    *
    * @param aFrame      The frame to insert into the buffer.
    *
    * @returns True if the decoder should decode another frame.
    */
   InsertStatus Insert(RefPtr<imgFrame>&& aFrame)
   {
     MOZ_ASSERT(mPending > 0);
@@ -380,12 +381,48 @@ private:
 
   // The frames of this animation, in order.
   nsTArray<RefPtr<imgFrame>> mFrames;
 
   // The maximum number of frames we can have before discarding.
   size_t mThreshold;
 };
 
+/**
+ * An AnimationFrameDiscardingQueue will only retain up to mBatch * 2 frames.
+ * When the animation advances, it will discard the old current frame.
+ */
+class AnimationFrameDiscardingQueue : public AnimationFrameBuffer
+{
+public:
+  explicit AnimationFrameDiscardingQueue(AnimationFrameRetainedBuffer&& aQueue);
+
+  imgFrame* Get(size_t aFrame, bool aForDisplay) final;
+  bool IsFirstFrameFinished() const final;
+  bool IsLastInsertedFrame(imgFrame* aFrame) const final;
+  bool MarkComplete(const gfx::IntRect& aFirstFrameRefreshArea) override;
+  void AddSizeOfExcludingThis(MallocSizeOf aMallocSizeOf,
+                              const AddSizeOfCb& aCallback) override;
+
+  const std::deque<RefPtr<imgFrame>>& Display() const { return mDisplay; }
+  const imgFrame* FirstFrame() const { return mFirstFrame; }
+  size_t PendingInsert() const { return mInsertIndex; }
+
+protected:
+  bool InsertInternal(RefPtr<imgFrame>&& aFrame) override;
+  void AdvanceInternal() override;
+  bool ResetInternal() override;
+
+  /// The sequential index of the frame we inserting next.
+  size_t mInsertIndex;
+
+  /// Queue storing frames to be displayed by the animator. The first frame in
+  /// the queue is the currently displayed frame.
+  std::deque<RefPtr<imgFrame>> mDisplay;
+
+  /// The first frame which is never discarded, and preferentially reused.
+  RefPtr<imgFrame> mFirstFrame;
+};
+
 } // namespace image
 } // namespace mozilla
 
 #endif // mozilla_image_AnimationFrameBuffer_h
--- a/image/AnimationSurfaceProvider.cpp
+++ b/image/AnimationSurfaceProvider.cpp
@@ -388,17 +388,27 @@ AnimationSurfaceProvider::CheckForNewFra
   }
 
   return continueDecoding;
 }
 
 void
 AnimationSurfaceProvider::RequestFrameDiscarding()
 {
-  MOZ_ASSERT_UNREACHABLE("Missing implementation");
+  mDecodingMutex.AssertCurrentThreadOwns();
+  mFramesMutex.AssertCurrentThreadOwns();
+  MOZ_ASSERT(mDecoder);
+
+  if (mFrames->MayDiscard()) {
+    MOZ_ASSERT_UNREACHABLE("Already replaced frame queue!");
+    return;
+  }
+
+  auto oldFrameQueue = static_cast<AnimationFrameRetainedBuffer*>(mFrames.get());
+  mFrames.reset(new AnimationFrameDiscardingQueue(std::move(*oldFrameQueue)));
 }
 
 void
 AnimationSurfaceProvider::AnnounceSurfaceAvailable()
 {
   mFramesMutex.AssertNotCurrentThreadOwns();
   MOZ_ASSERT(mImage);