Bug 1287691 (Part 3) - Yield after each frame in the PNG decoder. r=edwin
authorSeth Fowler <mark.seth.fowler@gmail.com>
Mon, 18 Jul 2016 23:51:16 -0700
changeset 330882 2448661e77258bcb13ba892a7e32c5a8ce0e584b
parent 330881 f0ec31db260e67c13526a33414cc8b860600b214
child 330883 f518461663cdd993b9c95a8a04d902d2427f40c0
push id9858
push userjlund@mozilla.com
push dateMon, 01 Aug 2016 14:37:10 +0000
treeherdermozilla-aurora@203106ef6cb6 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersedwin
bugs1287691
milestone50.0a1
Bug 1287691 (Part 3) - Yield after each frame in the PNG decoder. r=edwin
image/decoders/nsPNGDecoder.cpp
image/decoders/nsPNGDecoder.h
--- a/image/decoders/nsPNGDecoder.cpp
+++ b/image/decoders/nsPNGDecoder.cpp
@@ -21,16 +21,18 @@
 #include "png.h"
 #include "RasterImage.h"
 #include "SurfacePipeFactory.h"
 #include "mozilla/DebugOnly.h"
 #include "mozilla/Telemetry.h"
 
 using namespace mozilla::gfx;
 
+using std::min;
+
 namespace mozilla {
 namespace image {
 
 static LazyLogModule sPNGLog("PNGDecoder");
 static LazyLogModule sPNGDecoderAccountingLog("PNGDecoderAccounting");
 
 // Limit image dimensions (bug #251381, #591822, and #967656)
 #ifndef MOZ_PNG_MAX_DIMENSION
@@ -95,16 +97,18 @@ const uint8_t
 nsPNGDecoder::pngSignatureBytes[] = { 137, 80, 78, 71, 13, 10, 26, 10 };
 
 nsPNGDecoder::nsPNGDecoder(RasterImage* aImage)
  : Decoder(aImage)
  , mLexer(Transition::ToUnbuffered(State::FINISHED_PNG_DATA,
                                    State::PNG_DATA,
                                    SIZE_MAX),
           Transition::TerminateSuccess())
+ , mNextTransition(Transition::ContinueUnbuffered(State::PNG_DATA))
+ , mLastChunkLength(0)
  , mPNG(nullptr)
  , mInfo(nullptr)
  , mCMSLine(nullptr)
  , interlacebuf(nullptr)
  , mInProfile(nullptr)
  , mTransform(nullptr)
  , format(gfx::SurfaceFormat::UNKNOWN)
  , mCMSMode(0)
@@ -172,67 +176,66 @@ nsPNGDecoder::PostHasTransparencyIfNeede
         PostHasTransparency();
       }
       return;
   }
 }
 
 // CreateFrame() is used for both simple and animated images.
 nsresult
-nsPNGDecoder::CreateFrame(SurfaceFormat aFormat,
-                          const IntRect& aFrameRect,
-                          bool aIsInterlaced)
+nsPNGDecoder::CreateFrame(const FrameInfo& aFrameInfo)
 {
   MOZ_ASSERT(HasSize());
   MOZ_ASSERT(!IsMetadataDecode());
 
   // Check if we have transparency, and send notifications if needed.
-  auto transparency = GetTransparencyType(aFormat, aFrameRect);
+  auto transparency = GetTransparencyType(aFrameInfo.mFormat, aFrameInfo.mFrameRect);
   PostHasTransparencyIfNeeded(transparency);
   SurfaceFormat format = transparency == TransparencyType::eNone
                        ? SurfaceFormat::B8G8R8X8
                        : SurfaceFormat::B8G8R8A8;
 
   // Make sure there's no animation or padding if we're downscaling.
   MOZ_ASSERT_IF(mDownscaler, mNumFrames == 0);
   MOZ_ASSERT_IF(mDownscaler, !GetImageMetadata().HasAnimation());
   MOZ_ASSERT_IF(mDownscaler, transparency != TransparencyType::eFrameRect);
 
   IntSize targetSize = mDownscaler ? mDownscaler->TargetSize()
                                    : GetSize();
 
   // If this image is interlaced, we can display better quality intermediate
   // results to the user by post processing them with ADAM7InterpolatingFilter.
-  SurfacePipeFlags pipeFlags = aIsInterlaced
+  SurfacePipeFlags pipeFlags = aFrameInfo.mIsInterlaced
                              ? SurfacePipeFlags::ADAM7_INTERPOLATE
                              : SurfacePipeFlags();
 
   if (mNumFrames == 0) {
     // The first frame may be displayed progressively.
     pipeFlags |= SurfacePipeFlags::PROGRESSIVE_DISPLAY;
   }
 
   Maybe<SurfacePipe> pipe =
-    SurfacePipeFactory::CreateSurfacePipe(this, mNumFrames, GetSize(), targetSize,
-                                          aFrameRect, format, pipeFlags);
+    SurfacePipeFactory::CreateSurfacePipe(this, mNumFrames, GetSize(),
+                                          targetSize, aFrameInfo.mFrameRect,
+                                          format, pipeFlags);
 
   if (!pipe) {
     mPipe = SurfacePipe();
     return NS_ERROR_FAILURE;
   }
 
   mPipe = Move(*pipe);
 
-  mFrameRect = aFrameRect;
+  mFrameRect = aFrameInfo.mFrameRect;
   mPass = 0;
 
   MOZ_LOG(sPNGDecoderAccountingLog, LogLevel::Debug,
          ("PNGDecoderAccounting: nsPNGDecoder::CreateFrame -- created "
           "image frame with %dx%d pixels for decoder %p",
-          aFrameRect.width, aFrameRect.height, this));
+          mFrameRect.width, mFrameRect.height, this));
 
 #ifdef PNG_APNG_SUPPORTED
   if (png_get_valid(mPNG, mInfo, PNG_INFO_acTL)) {
     mAnimInfo = AnimFrameInfo(mPNG, mInfo);
 
     if (mAnimInfo.mDispose == DisposalMethod::CLEAR) {
       // We may have to display the background under this image during
       // animation playback, so we regard it as transparent.
@@ -363,36 +366,47 @@ nsPNGDecoder::DoDecode(SourceBufferItera
     }
     MOZ_CRASH("Unknown State");
   });
 }
 
 LexerTransition<nsPNGDecoder::State>
 nsPNGDecoder::ReadPNGData(const char* aData, size_t aLength)
 {
+  // If we were waiting until after returning from a yield to call
+  // CreateFrame(), call it now.
+  if (mNextFrameInfo) {
+    if (NS_FAILED(CreateFrame(*mNextFrameInfo))) {
+      return Transition::TerminateFailure();
+    }
+
+    MOZ_ASSERT(mImageData, "Should have a buffer now");
+    mNextFrameInfo = Nothing();
+  }
+
   // libpng uses setjmp/longjmp for error handling.
   if (setjmp(png_jmpbuf(mPNG))) {
     return Transition::TerminateFailure();
   }
 
   // Pass the data off to libpng.
+  mLastChunkLength = aLength;
+  mNextTransition = Transition::ContinueUnbuffered(State::PNG_DATA);
   png_process_data(mPNG, mInfo,
                    reinterpret_cast<unsigned char*>(const_cast<char*>((aData))),
                    aLength);
 
-  if (HasError()) {
-    return Transition::TerminateFailure();
-  }
+  // Make sure that we've reached a terminal state if decoding is done.
+  MOZ_ASSERT_IF(GetDecodeDone(), mNextTransition.NextStateIsTerminal());
+  MOZ_ASSERT_IF(HasError(), mNextTransition.NextStateIsTerminal());
 
-  if (GetDecodeDone()) {
-    return Transition::TerminateSuccess();
-  }
-
-  // Keep reading data.
-  return Transition::ContinueUnbuffered(State::PNG_DATA);
+  // Continue with whatever transition the callback code requested. We
+  // initialized this to Transition::ContinueUnbuffered(State::PNG_DATA) above,
+  // so by default we just continue the unbuffered read.
+  return mNextTransition;
 }
 
 LexerTransition<nsPNGDecoder::State>
 nsPNGDecoder::FinishedPNGData()
 {
   // Since we set up an unbuffered read for SIZE_MAX bytes, if we actually read
   // all that data something is really wrong.
   MOZ_ASSERT_UNREACHABLE("Read the entire address space?");
@@ -678,31 +692,32 @@ nsPNGDecoder::info_callback(png_structp 
     // call PostHasTransparency in the metadata decode if we need to. So it's okay
     // to pass IntRect(0, 0, width, height) here for animated images; they will
     // call with the proper first frame rect in the full decode.
     auto transparency = decoder->GetTransparencyType(decoder->format, frameRect);
     decoder->PostHasTransparencyIfNeeded(transparency);
 
     // We have the metadata we're looking for, so stop here, before we allocate
     // buffers below.
-    png_process_data_pause(png_ptr, /* save = */ false);
-    return;
+    return decoder->DoTerminate(png_ptr, TerminalState::SUCCESS);
   }
 
 #ifdef PNG_APNG_SUPPORTED
   if (isAnimated) {
     png_set_progressive_frame_fn(png_ptr, nsPNGDecoder::frame_info_callback,
                                  nullptr);
   }
 
   if (png_get_first_frame_is_hidden(png_ptr, info_ptr)) {
     decoder->mFrameIsHidden = true;
   } else {
 #endif
-    nsresult rv = decoder->CreateFrame(decoder->format, frameRect, isInterlaced);
+    nsresult rv = decoder->CreateFrame(FrameInfo{ decoder->format,
+                                                  frameRect,
+                                                  isInterlaced });
     if (NS_FAILED(rv)) {
       png_longjmp(decoder->mPNG, 5); // NS_ERROR_OUT_OF_MEMORY
     }
     MOZ_ASSERT(decoder->mImageData, "Should have a buffer now");
 #ifdef PNG_APNG_SUPPORTED
   }
 #endif
 
@@ -894,50 +909,83 @@ nsPNGDecoder::WriteRow(uint8_t* aRow)
       png_longjmp(mPNG, 1);  // Abort the decode.
   }
 
   MOZ_ASSERT(WriteState(result) != WriteState::FAILURE);
 
   PostInvalidationIfNeeded();
 }
 
+void
+nsPNGDecoder::DoTerminate(png_structp aPNGStruct, TerminalState aState)
+{
+  // Stop processing data. Note that we intentionally ignore the return value of
+  // png_process_data_pause(), which tells us how many bytes of the data that
+  // was passed to png_process_data() have not been consumed yet, because now
+  // that we've reached a terminal state, we won't do any more decoding or call
+  // back into libpng anymore.
+  png_process_data_pause(aPNGStruct, /* save = */ false);
+
+  mNextTransition = aState == TerminalState::SUCCESS
+                  ? Transition::TerminateSuccess()
+                  : Transition::TerminateFailure();
+}
+
+void
+nsPNGDecoder::DoYield(png_structp aPNGStruct)
+{
+  // Pause data processing. png_process_data_pause() returns how many bytes of
+  // the data that was passed to png_process_data() have not been consumed yet.
+  // We use this information to tell StreamingLexer where to place us in the
+  // input stream when we come back from the yield.
+  png_size_t pendingBytes = png_process_data_pause(aPNGStruct, /* save = */ false);
+
+  MOZ_ASSERT(pendingBytes < mLastChunkLength);
+  size_t consumedBytes = mLastChunkLength - min(pendingBytes, mLastChunkLength);
+
+  mNextTransition =
+    Transition::ContinueUnbufferedAfterYield(State::PNG_DATA, consumedBytes);
+}
+
 #ifdef PNG_APNG_SUPPORTED
 // got the header of a new frame that's coming
 void
 nsPNGDecoder::frame_info_callback(png_structp png_ptr, png_uint_32 frame_num)
 {
   nsPNGDecoder* decoder =
                static_cast<nsPNGDecoder*>(png_get_progressive_ptr(png_ptr));
 
   // old frame is done
   decoder->EndImageFrame();
 
   if (!decoder->mFrameIsHidden && decoder->IsFirstFrameDecode()) {
     // We're about to get a second non-hidden frame, but we only want the first.
     // Stop decoding now. (And avoid allocating the unnecessary buffers below.)
     decoder->PostDecodeDone();
-    png_process_data_pause(png_ptr, /* save = */ false);
-    return;
+    return decoder->DoTerminate(png_ptr, TerminalState::SUCCESS);
   }
 
   // Only the first frame can be hidden, so unhide unconditionally here.
   decoder->mFrameIsHidden = false;
 
+  // Save the information necessary to create the frame; we'll actually create
+  // it when we return from the yield.
   const IntRect frameRect(png_get_next_frame_x_offset(png_ptr, decoder->mInfo),
                           png_get_next_frame_y_offset(png_ptr, decoder->mInfo),
                           png_get_next_frame_width(png_ptr, decoder->mInfo),
                           png_get_next_frame_height(png_ptr, decoder->mInfo));
 
   const bool isInterlaced = bool(decoder->interlacebuf);
 
-  nsresult rv = decoder->CreateFrame(decoder->format, frameRect, isInterlaced);
-  if (NS_FAILED(rv)) {
-    png_longjmp(decoder->mPNG, 5); // NS_ERROR_OUT_OF_MEMORY
-  }
-  MOZ_ASSERT(decoder->mImageData, "Should have a buffer now");
+  decoder->mNextFrameInfo = Some(FrameInfo{ decoder->format,
+                                            frameRect,
+                                            isInterlaced });
+
+  // Yield to the caller to notify them that the previous frame is now complete.
+  return decoder->DoYield(png_ptr);
 }
 #endif
 
 void
 nsPNGDecoder::end_callback(png_structp png_ptr, png_infop info_ptr)
 {
   /* libpng comments:
    *
@@ -960,19 +1008,20 @@ nsPNGDecoder::end_callback(png_structp p
   int32_t loop_count = 0;
 #ifdef PNG_APNG_SUPPORTED
   if (png_get_valid(png_ptr, info_ptr, PNG_INFO_acTL)) {
     int32_t num_plays = png_get_num_plays(png_ptr, info_ptr);
     loop_count = num_plays - 1;
   }
 #endif
 
-  // Send final notifications
+  // Send final notifications.
   decoder->EndImageFrame();
   decoder->PostDecodeDone(loop_count);
+  return decoder->DoTerminate(png_ptr, TerminalState::SUCCESS);
 }
 
 
 void
 nsPNGDecoder::error_callback(png_structp png_ptr, png_const_charp error_msg)
 {
   MOZ_LOG(sPNGLog, LogLevel::Error, ("libpng error: %s\n", error_msg));
   png_longjmp(png_ptr, 1);
--- a/image/decoders/nsPNGDecoder.h
+++ b/image/decoders/nsPNGDecoder.h
@@ -31,19 +31,25 @@ public:
   bool IsValidICO() const;
 
 private:
   friend class DecoderFactory;
 
   // Decoders should only be instantiated via DecoderFactory.
   explicit nsPNGDecoder(RasterImage* aImage);
 
-  nsresult CreateFrame(gfx::SurfaceFormat aFormat,
-                       const gfx::IntRect& aFrameRect,
-                       bool aIsInterlaced);
+  /// The information necessary to create a frame.
+  struct FrameInfo
+  {
+    gfx::SurfaceFormat mFormat;
+    gfx::IntRect mFrameRect;
+    bool mIsInterlaced;
+  };
+
+  nsresult CreateFrame(const FrameInfo& aFrameInfo);
   void EndImageFrame();
 
   enum class TransparencyType
   {
     eNone,
     eAlpha,
     eFrameRect
   };
@@ -51,27 +57,46 @@ private:
   TransparencyType GetTransparencyType(gfx::SurfaceFormat aFormat,
                                        const gfx::IntRect& aFrameRect);
   void PostHasTransparencyIfNeeded(TransparencyType aTransparencyType);
 
   void PostInvalidationIfNeeded();
 
   void WriteRow(uint8_t* aRow);
 
+  // Convenience methods to make interacting with StreamingLexer from inside
+  // a libpng callback easier.
+  void DoTerminate(png_structp aPNGStruct, TerminalState aState);
+  void DoYield(png_structp aPNGStruct);
+
   enum class State
   {
     PNG_DATA,
     FINISHED_PNG_DATA
   };
 
   LexerTransition<State> ReadPNGData(const char* aData, size_t aLength);
   LexerTransition<State> FinishedPNGData();
 
   StreamingLexer<State> mLexer;
 
+  // The next lexer state transition. We need to store it here because we can't
+  // directly return arbitrary values from libpng callbacks.
+  LexerTransition<State> mNextTransition;
+
+  // We yield to the caller every time we finish decoding a frame. When this
+  // happens, we need to allocate the next frame after returning from the yield.
+  // |mNextFrameInfo| is used to store the information needed to allocate the
+  // next frame.
+  Maybe<FrameInfo> mNextFrameInfo;
+
+  // The length of the last chunk of data passed to ReadPNGData(). We use this
+  // to arrange to arrive back at the correct spot in the data after yielding.
+  size_t mLastChunkLength;
+
 public:
   png_structp mPNG;
   png_infop mInfo;
   nsIntRect mFrameRect;
   uint8_t* mCMSLine;
   uint8_t* interlacebuf;
   qcms_profile* mInProfile;
   qcms_transform* mTransform;