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 345789 2fd2f3d22e5d391cd2cfaa2a3cefa0978e0fc8a5
parent 345788 1bd6857c728d4345f870ecc9ba896c3568690d71
child 345790 fe259a08d225f4b533fac6c66329acc6c7d65388
push id6389
push userraliiev@mozilla.com
push dateMon, 19 Sep 2016 13:38:22 +0000
treeherdermozilla-beta@01d67bfe6c81 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersedwin
bugs1287691
milestone50.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 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->Terminate(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::Terminate(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::Yield(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->Terminate(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->Yield(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->Terminate(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 Terminate(png_structp aPNGStruct, TerminalState aState);
+  void Yield(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;