Image Load Cancelation - Coarse cancelation of in-flight jpeg loads
authorAndrew Cox <andrew.cox@partner.samsung.com>
Thu, 5 Jun 2014 20:23:47 +0000 (21:23 +0100)
committerAdeel Kazmi <adeel.kazmi@samsung.com>
Tue, 10 Jun 2014 16:00:12 +0000 (17:00 +0100)
[problem] An itemview with many large image items can load lots that
never need to be displayed in a fast-scroll situation, leading to
increased latency to load required images and wasted power doing
unecessary work.
While we already issue cancelations for queued images, if an image load
request has been dequeued to a resource thread, it will proceed
through file IO, decoding, and postprocessing.

[solution]
Add a generic cancelation mechanism for the current load on any resource
thread using boost::thread's interrupt().

Made the image resource thread exception safe on its load and decode
paths so this boost interrupt() can be used in their scopes.

Injected interruption points into the jpeg turbo loader. Key ones:
  * Between allocation of a memory buffer and file IO, and
    running the jpeg codec.
  * Between the codec and the pixel shuffling for orientation compensation.

Change-Id: I4a8c10f8f9cd1e11f04285ac3f72515a47cf604e
Signed-off-by: Andrew Cox <andrew.cox@partner.samsung.com>
build/tizen/common/Makefile.am
build/tizen/dali-adaptor/Makefile.am
build/tizen/dali-application/Makefile.am
build/tizen/dali-evas/Makefile.am
build/tizen/dali-livebox/Makefile.am
build/tizen/dali-native-buffer/Makefile.am
platform-abstractions/portable/file-closer.h [new file with mode: 0644]
platform-abstractions/slp/resource-loader/loader-jpeg-turbo.cpp
platform-abstractions/slp/resource-loader/resource-thread-base.cpp
platform-abstractions/slp/resource-loader/resource-thread-base.h
platform-abstractions/slp/resource-loader/resource-thread-image.cpp

index 9dcdb32..c6a0b8e 100644 (file)
@@ -74,13 +74,15 @@ libdali_adaptor_common_la_SOURCES = \
 
 libdali_adaptor_common_la_DEPENDENCIES =
 
+# List include directories with more platform-specific (slp) before portable root:
 libdali_adaptor_common_la_includes = \
                       -I../../.. \
-                      -I../../../platform-abstractions/ \
                       -I../../../platform-abstractions/slp \
                       -I../../../platform-abstractions/slp/resource-loader \
-                      -I../../../adaptors/ \
+                      -I../../../platform-abstractions/portable \
+                      -I../../../platform-abstractions/ \
                       -I../../../adaptors/tizen \
+                      -I../../../adaptors/ \
                       -I../../../capi/
 
 daliDefaultFontCacheDir = ${dataReadOnlyDir}/glyphcache/
index 1d8e200..ec1183d 100644 (file)
@@ -53,11 +53,14 @@ endif
 
 libdali_adaptor_la_DEPENDENCIES =
 
+# List include directories with more platform-specific (slp) before portable
+# so platform-specific files can override portable versions:
 libdali_adaptor_includes = \
                       -I../../.. \
                       -I../../../platform-abstractions/ \
                       -I../../../platform-abstractions/slp \
                       -I../../../platform-abstractions/slp/resource-loader \
+                      -I../../../platform-abstractions/portable \
                       -I../../../adaptors/ \
                       -I../../../adaptors/tizen \
                       -I../../../capi
index e33d88f..425ad8b 100644 (file)
@@ -38,6 +38,7 @@ libdali_application_includes = \
                       -I../../../platform-abstractions/ \
                       -I../../../platform-abstractions/slp \
                       -I../../../platform-abstractions/slp/resource-loader \
+                      -I../../../platform-abstractions/portable \
                       -I../../../adaptors/ \
                       -I../../../adaptors/tizen \
                       -I../../../capi
index 938f36a..5529f6e 100644 (file)
@@ -38,6 +38,7 @@ libdali_evas_plugin_includes = \
                       -I../../../platform-abstractions/ \
                       -I../../../platform-abstractions/slp \
                       -I../../../platform-abstractions/slp/resource-loader \
+                      -I../../../platform-abstractions/portable \
                       -I../../../adaptors/ \
                       -I../../../adaptors/tizen \
                       -I../../../capi/
index 29f938c..8a97506 100644 (file)
@@ -38,6 +38,7 @@ libdali_livebox_plugin_includes = \
                       -I../../../platform-abstractions/ \
                       -I../../../platform-abstractions/slp \
                       -I../../../platform-abstractions/slp/resource-loader \
+                      -I../../../platform-abstractions/portable \
                       -I../../../adaptors/ \
                       -I../../../adaptors/tizen \
                       -I../../../adaptors/tizen\common
index c34c666..55e4c93 100644 (file)
@@ -38,6 +38,7 @@ libdali_native_buffer_plugin_includes = \
                       -I../../../platform-abstractions/ \
                       -I../../../platform-abstractions/slp \
                       -I../../../platform-abstractions/slp/resource-loader \
+                      -I../../../platform-abstractions/portable \
                       -I../../../adaptors/ \
                       -I../../../adaptors/tizen \
                       -I../../../capi
diff --git a/platform-abstractions/portable/file-closer.h b/platform-abstractions/portable/file-closer.h
new file mode 100644 (file)
index 0000000..63b03eb
--- /dev/null
@@ -0,0 +1,103 @@
+#ifndef _DALI_INTERNAL_PLATFORM_FILECLOSER_H__
+#define _DALI_INTERNAL_PLATFORM_FILECLOSER_H__
+/*
+ * Copyright (c) 2014 Samsung Electronics Co., Ltd.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+// INTERNAL INCLUDES
+
+// EXTERNAL INCLUDES
+#include <cstdio>
+
+namespace Dali
+{
+namespace Internal
+{
+namespace Platform
+{
+
+/**
+ * Opens files and closes them later even if an exception is thrown.
+ */
+class FileCloser
+{
+public:
+
+  /**
+   * @brief Construct a FileCloser guarding a new FILE* for accessing the path passed in.
+   */
+  FileCloser( const char * const filename, const char * const mode ) :
+    mFile(fopen(filename, mode))
+  {
+    DALI_ASSERT_DEBUG( filename != 0 && "Cant open a null filename." );
+    DALI_ASSERT_DEBUG( mode != 0 && "Null mode is undefined behaviour in spec." );
+
+    if( mFile == 0 )
+    {
+      DALI_LOG_WARNING( "File open failed for: \"%s\" in mode: \"%s\".\n", filename, mode );
+    }
+  }
+
+  /**
+   * @brief Construct a FileCloser guarding a FILE* for reading out of the memory buffer passed in.
+   */
+  FileCloser( void * const buffer, const size_t bufferSize, const char * const mode ) :
+    mFile( fmemopen( buffer, bufferSize, mode ) )
+  {
+    DALI_ASSERT_DEBUG( buffer != 0 && "Cant open file on null buffer." );
+    DALI_ASSERT_DEBUG( bufferSize > 0 && "Pointless to open file on empty buffer." );
+    DALI_ASSERT_DEBUG( mode != 0 && "Null mode is undefined behaviour in spec." );
+
+    if( mFile == 0 )
+    {
+      DALI_LOG_WARNING( "File open failed for memory buffer at location: \"%p\", of size: \"%u\", in mode: \"%s\".\n", static_cast<void*>(buffer), static_cast<unsigned>(bufferSize), mode );
+    }
+  }
+
+   /**
+    * @brief Destroy the FileCloser and clean up its FILE*.
+    */
+  ~FileCloser()
+  {
+    if( mFile != 0 )
+    {
+      const int closeFailed = fclose( mFile );
+
+      if ( closeFailed )
+      {
+        DALI_LOG_WARNING( "File close failed for FILE: \"%p\".\n", static_cast<void*>(mFile) );
+      }
+      mFile = 0;
+    }
+  }
+
+  /**
+   * @return The FILE* guarded by this object.
+   */
+  FILE* GetFile()
+  {
+    return mFile;
+  }
+
+private:
+  FILE* mFile;
+};
+
+} /* namespace Platform */
+} /* namespace Internal */
+} /* namespace Dali */
+
+#endif /* _DALI_INTERNAL_PLATFORM_FILECLOSER_H__ */
index f3084f9..cdaa224 100755 (executable)
@@ -29,6 +29,7 @@
 #include <libexif/exif-loader.h>
 #include <libexif/exif-tag.h>
 #include <setjmp.h>
+#include <boost/thread.hpp>
 
 namespace Dali
 {
@@ -279,14 +280,6 @@ bool LoadBitmapFromJpeg( FILE *fp, Bitmap& bitmap, ImageAttributes& attributes )
   }
   unsigned char * const jpegBufferPtr = &jpegBuffer[0];
 
-  AutoJpg autoJpg(tjInitDecompress());
-
-  if(autoJpg.GetHandle() == NULL)
-  {
-    DALI_LOG_ERROR("%s\n", tjGetErrorStr());
-    return false;
-  }
-
   // Pull the compressed JPEG image bytes out of a file and into memory:
   if( fread( jpegBufferPtr, 1, jpegBufferSize, fp ) != jpegBufferSize )
   {
@@ -299,6 +292,17 @@ bool LoadBitmapFromJpeg( FILE *fp, Bitmap& bitmap, ImageAttributes& attributes )
     DALI_LOG_ERROR("Error seeking to start of file\n");
   }
 
+  // Allow early cancellation between the load and the decompress:
+  boost::this_thread::interruption_point(); ///@warning This can throw an exception.
+
+  AutoJpg autoJpg(tjInitDecompress());
+
+  if(autoJpg.GetHandle() == NULL)
+  {
+    DALI_LOG_ERROR("%s\n", tjGetErrorStr());
+    return false;
+  }
+
   JPGFORM_CODE transform = JPGFORM_NONE;
 
   if( attributes.GetOrientationCorrection() )
@@ -348,6 +352,9 @@ bool LoadBitmapFromJpeg( FILE *fp, Bitmap& bitmap, ImageAttributes& attributes )
 
   unsigned char * const bitmapPixelBuffer =  bitmap.GetPackedPixelsProfile()->ReserveBuffer(Pixel::RGB888, scaledPostXformWidth, scaledPostXformHeight);
 
+  // Allow early cancellation before decoding:
+  boost::this_thread::interruption_point(); ///@warning This can throw an exception.
+
   const int pitch = scaledPreXformWidth * DECODED_PIXEL_SIZE;
   if( tjDecompress2( autoJpg.GetHandle(), jpegBufferPtr, jpegBufferSize, bitmapPixelBuffer, scaledPreXformWidth, pitch, scaledPreXformHeight, DECODED_PIXEL_LIBJPEG_TYPE, flags ) == -1 )
   {
@@ -361,6 +368,12 @@ bool LoadBitmapFromJpeg( FILE *fp, Bitmap& bitmap, ImageAttributes& attributes )
   const unsigned int  bufferWidth  = GetTextureDimension( scaledPreXformWidth );
   const unsigned int  bufferHeight = GetTextureDimension( scaledPreXformHeight );
 
+  if( transform != JPGFORM_NONE )
+  {
+    // Allow early cancellation before shuffling pixels around on the CPU:
+    boost::this_thread::interruption_point(); ///@warning This can throw an exception.
+  }
+
   bool result = false;
   switch(transform)
   {
index 5a1250e..248eb93 100644 (file)
@@ -28,11 +28,13 @@ using boost::scoped_ptr;
 namespace Dali
 {
 
+const Integration::ResourceId NO_REQUEST = Integration::ResourceId(0) - 1;
+
 namespace SlpPlatform
 {
 
 ResourceThreadBase::ResourceThreadBase(ResourceLoader& resourceLoader)
-: mResourceLoader(resourceLoader), mPaused(false)
+: mResourceLoader( resourceLoader ), mCurrentRequestId( NO_REQUEST ), mPaused( false )
 {
 #if defined(DEBUG_ENABLED)
   mLogFilter = Debug::Filter::New(Debug::Concise, false, "LOG_RESOURCE_THREAD_BASE");
@@ -93,14 +95,23 @@ void ResourceThreadBase::CancelRequest( Integration::ResourceId resourceId )
     // Lock while searching and removing from the request queue:
     unique_lock<mutex> lock( mMutex );
 
-    for( RequestQueueIter iterator = mQueue.begin();
-         iterator != mQueue.end();
-         ++iterator )
+    // See if the request is already launched as the current job on the thread:
+    if( mCurrentRequestId == resourceId )
+    {
+      mThread->interrupt();
+    }
+    // Check the pending requests to be cancelled:
+    else
     {
-      if( ((*iterator).first).GetId() == resourceId )
+      for( RequestQueueIter iterator = mQueue.begin();
+           iterator != mQueue.end();
+           ++iterator )
       {
-        iterator = mQueue.erase( iterator );
-        break;
+        if( ((*iterator).first).GetId() == resourceId )
+        {
+          iterator = mQueue.erase( iterator );
+          break;
+        }
       }
     }
   }
@@ -138,18 +149,48 @@ void ResourceThreadBase::ThreadLoop()
 
   while( !mResourceLoader.IsTerminating() )
   {
-    WaitForRequests();
+    try
+    {
+      WaitForRequests();
+
+      if ( !mResourceLoader.IsTerminating() )
+      {
+        ProcessNextRequest();
+      }
+    }
 
-    if ( !mResourceLoader.IsTerminating() )
+    catch( boost::thread_interrupted& ex )
     {
-      ProcessNextRequest();
+      // No problem, thread was just interrupted from the outside to cancel an in-flight request.
+      boost::thread_interrupted* disableUnusedVarWarning = &ex;
+      ex = *disableUnusedVarWarning;
+    }
+
+    // Since we have an exception handler here anyway, lets catch everything to avoid killing the process:
+    catch( std::exception& ex )
+    {
+      const char * const what = ex.what();
+      DALI_LOG_ERROR( "std::exception caught in resource thread. Aborting request with id %u because of std::exception with reason, \"%s\".\n", unsigned(mCurrentRequestId), what ? what : "null" );
+
+    }
+    catch( Dali::DaliException& ex )
+    {
+      // Probably a failed assert-always:
+      DALI_LOG_ERROR( "DaliException caught in resource thread. Aborting request with id %u. Location: \"%s\". Condition: \"%s\".\n", unsigned(mCurrentRequestId), ex.mLocation.c_str(), ex.mCondition.c_str() );
+    }
+    catch( ... )
+    {
+      DALI_LOG_ERROR( "Unknown exception caught in resource thread. Aborting request with id %u.\n", unsigned(mCurrentRequestId) );
     }
   }
 }
 
 void ResourceThreadBase::WaitForRequests()
 {
-  unique_lock<mutex> lock(mMutex);
+  unique_lock<mutex> lock( mMutex );
+
+  // Clear the previously current request:
+  mCurrentRequestId = NO_REQUEST;
 
   if( mQueue.empty() || mPaused == true )
   {
@@ -173,6 +214,7 @@ void ResourceThreadBase::ProcessNextRequest()
       const RequestInfo & front = mQueue.front();
       request = new ResourceRequest( front.first );
       type = front.second;
+      mCurrentRequestId = front.first.GetId();
       mQueue.pop_front();
     }
   } // unlock the queue
@@ -201,6 +243,13 @@ void ResourceThreadBase::ProcessNextRequest()
       }
       break;
     }
+
+    // Clear the interruption status for derived classes that don't implement on-the-fly cancellation yet:
+    boost::this_thread::interruption_point(); ///@warning This can throw an exception.
+    // To support cancellation of an in-flight resource, place the above line at key points in derived
+    // resource thread classes and the loading / decoding / saving code that they call.
+    // See resource-thread-image.cpp and loader-jpeg-turbo.cpp for a conservative example of its use.
+    ///@note: The above line will throw an exception so only place it in exception-safe locations.
   }
 }
 
index 8636e54..ffabc67 100644 (file)
@@ -140,7 +140,8 @@ protected:
   boost::condition_variable mCondition;         ///< condition variable
   boost::mutex mMutex;                          ///< used to protect mQueue
   RequestQueue mQueue;                          ///< Request queue
-  bool mPaused;                                 ///< Whether to process work in mQueue
+  Integration::ResourceId mCurrentRequestId;    ///< Request being processed on thread
+  bool mPaused;                                ///< Whether to process work in mQueue
 
 #if defined(DEBUG_ENABLED)
 public:
index c1882cf..10bb127 100755 (executable)
@@ -30,6 +30,8 @@
 #include "loader-ktx.h"
 #include "loader-wbmp.h"
 
+#include "file-closer.h"
+
 using namespace std;
 using namespace Dali::Integration;
 using boost::mutex;
@@ -367,14 +369,19 @@ void ResourceThreadImage::Load(const ResourceRequest& request)
   DALI_LOG_TRACE_METHOD( mLogFilter );
   DALI_LOG_INFO( mLogFilter, Debug::Verbose, "%s(%s)\n", __FUNCTION__, request.GetPath().c_str() );
 
-  bool file_not_found = false;
+  bool fileNotFound = false;
   BitmapPtr bitmap = 0;
   bool result = false;
 
-  FILE * const fp = fopen( request.GetPath().c_str(), "rb" );
+  Dali::Internal::Platform::FileCloser fileCloser( request.GetPath().c_str(), "rb" );
+  FILE * const fp = fileCloser.GetFile();
+
   if( fp != NULL )
   {
     result = ConvertStreamToBitmap( *request.GetType(), request.GetPath(), fp, bitmap );
+    // Last chance to interrupt a cancelled load before it is reported back to clients
+    // which have already stopped tracking it:
+    boost::this_thread::interruption_point(); ///@warning This can throw an exception.
     if( result && bitmap )
     {
       // Construct LoadedResource and ResourcePointer for image data
@@ -390,12 +397,12 @@ void ResourceThreadImage::Load(const ResourceRequest& request)
   else
   {
     DALI_LOG_WARNING( "Failed to open file to load \"%s\"\n", request.GetPath().c_str() );
-    file_not_found = true;
+    fileNotFound = true;
   }
 
   if ( !bitmap )
   {
-    if( file_not_found )
+    if( fileNotFound )
     {
       FailedResource resource(request.GetId(), FailureFileNotFound  );
       mResourceLoader.AddFailedLoad(resource);
@@ -431,7 +438,8 @@ void ResourceThreadImage::Decode(const ResourceRequest& request)
     if( blobBytes != 0 && blobSize > 0U )
     {
       // Open a file handle on the memory buffer:
-      FILE * const fp = fmemopen(blobBytes, blobSize, "rb");
+      Dali::Internal::Platform::FileCloser fileCloser( blobBytes, blobSize, "rb" );
+      FILE * const fp = fileCloser.GetFile();
       if ( fp != NULL )
       {
         bool result = ConvertStreamToBitmap( *request.GetType(), request.GetPath(), fp, bitmap );
@@ -492,6 +500,9 @@ bool ResourceThreadImage::ConvertStreamToBitmap(const ResourceType& resourceType
       const BitmapResourceType& resType = static_cast<const BitmapResourceType&>(resourceType);
       ImageAttributes attributes  = resType.imageAttributes;
 
+      // Check for cancellation now we have hit the filesystem, done some allocation, and burned some cycles:
+      boost::this_thread::interruption_point(); ///@warning This can throw an exception.
+
       result = function(fp, *bitmap, attributes);
 
       if (!result)
@@ -504,7 +515,6 @@ bool ResourceThreadImage::ConvertStreamToBitmap(const ResourceType& resourceType
     {
       DALI_LOG_WARNING("Image Decoder for %s unavailable\n", path.c_str());
     }
-    fclose(fp); ///! Not exception safe, but an exception on a resource thread will bring the process down anyway.
   }
 
   ptr.Reset( bitmap.Get() );