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 442380 453d21db2a306f4800e8be4d77f6d61000cb9a0c
parent 442379 bd9168e1e48ce09404664e250fdc151d301b98db
child 442381 9c8e6aaa017625dd61942d3100316798c8c2ef61
push id109156
push useraosmond@gmail.com
push dateMon, 22 Oct 2018 17:41:47 +0000
treeherdermozilla-inbound@2ce8f6d1a64e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerstnikkel
bugs1465619
milestone65.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 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);