← Back to team overview

openlp-core team mailing list archive

[Merge] lp:~googol/openlp/image-queue into lp:openlp

 

Andreas Preikschat has proposed merging lp:~googol/openlp/image-queue into lp:openlp.

Requested reviews:
  Tim Bentley (trb143)
  Raoul Snyman (raoul-snyman)

For more details, see:
https://code.launchpad.net/~googol/openlp/image-queue/+merge/65969

Hello,

I changed the image manager in two ways:
1) the resize_image is now done in the thread. Now you can add images to the service manager without delay and the thread resizes and creates the bytes streams in the background)

2) implemented an 'intelligent' image queue to reduce the possibility that we are generating data (e. g. the byte stream) when it is not needed instead we try to process those images which are likely needed earlier first.
-- 
https://code.launchpad.net/~googol/openlp/image-queue/+merge/65969
Your team OpenLP Core is subscribed to branch lp:openlp.
=== modified file 'openlp/core/lib/imagemanager.py'
--- openlp/core/lib/imagemanager.py	2011-06-12 16:02:52 +0000
+++ openlp/core/lib/imagemanager.py	2011-06-27 11:52:31 +0000
@@ -32,6 +32,7 @@
 """
 import logging
 import time
+import Queue
 
 from PyQt4 import QtCore
 
@@ -42,8 +43,8 @@
 
 class ImageThread(QtCore.QThread):
     """
-    A special Qt thread class to speed up the display of text based frames.
-    This is threaded so it loads the frames in background
+    A special Qt thread class to speed up the display of images. This is
+    threaded so it loads the frames and generates byte stream in background.
     """
     def __init__(self, manager):
         QtCore.QThread.__init__(self, None)
@@ -53,15 +54,75 @@
         """
         Run the thread.
         """
-        self.imageManager.process()
+        self.imageManager._process()
+
+
+class Priority(object):
+    """
+    Enumeration class for different priorities.
+
+    ``Lowest``
+        Only the image's byte stream has to be generated. But neither the
+        ``QImage`` nor the byte stream has been requested yet.
+
+    ``Low``
+        Only the image's byte stream has to be generated. Because the image's
+        ``QImage`` has been requested previously it is reasonable to assume that
+        the byte stream will be needed before the byte stream of other images
+        whose ``QImage`` were not generated due to a request.
+
+    ``Normal``
+        The image's byte stream as well as the image has to be generated.
+        Neither the ``QImage`` nor the byte stream has been requested yet.
+
+    ``High``
+        The image's byte stream as well as the image has to be generated. The
+        ``QImage`` for this image has been requested.
+        **Note**, this priority is only set when the ``QImage`` has not been
+        generated yet.
+
+    ``Urgent``
+        The image's byte stream as well as the image has to be generated. The
+        byte stream for this image has been requested.
+        **Note**, this priority is only set when the byte stream has not been
+        generated yet.
+    """
+    Lowest = 4
+    Low = 3
+    Normal = 2
+    High = 1
+    Urgent = 0
 
 
 class Image(object):
-    name = ''
-    path = ''
-    dirty = True
-    image = None
-    image_bytes = None
+    """
+    This class represents an image. To mark an image as *dirty* set the instance
+    variables ``image`` and ``image_bytes`` to ``None`` and add the image object
+    to the queue of images to process.
+    """
+    def __init__(self, name='', path=''):
+        self.name = name
+        self.path = path
+        self.image = None
+        self.image_bytes = None
+        self.priority = Priority.Normal
+
+
+class PriorityQueue(Queue.PriorityQueue):
+    """
+    Customised ``Queue.PriorityQueue``.
+    """
+    def remove(self, item):
+        """
+        Removes the given ``item`` from the queue.
+
+        ``item``
+            The item to remove. This should be a tuple::
+
+                ``(Priority, Image)``
+        """
+        if item in self.queue:
+            self.queue.remove(item)
 
 
 class ImageManager(QtCore.QObject):
@@ -76,96 +137,120 @@
         self.width = current_screen[u'size'].width()
         self.height = current_screen[u'size'].height()
         self._cache = {}
-        self._thread_running = False
-        self._cache_dirty = False
-        self.image_thread = ImageThread(self)
+        self._imageThread = ImageThread(self)
+        self._conversion_queue = PriorityQueue()
 
     def update_display(self):
         """
-        Screen has changed size so rebuild the cache to new size
+        Screen has changed size so rebuild the cache to new size.
         """
         log.debug(u'update_display')
         current_screen = ScreenList.get_instance().current
         self.width = current_screen[u'size'].width()
         self.height = current_screen[u'size'].height()
-        # mark the images as dirty for a rebuild
-        for key in self._cache.keys():
-            image = self._cache[key]
-            image.dirty = True
-            image.image = resize_image(image.path, self.width, self.height)
-        self._cache_dirty = True
-        # only one thread please
-        if not self._thread_running:
-            self.image_thread.start()
+        # Mark the images as dirty for a rebuild by setting the image and byte
+        # stream to None.
+        self._conversion_queue = PriorityQueue()
+        for key, image in self._cache.iteritems():
+            image.priority = Priority.Normal
+            image.image = None
+            image.image_bytes = None
+            self._conversion_queue.put((image.priority, image))
+        # We want only one thread.
+        if not self._imageThread.isRunning():
+            self._imageThread.start()
 
     def get_image(self, name):
         """
-        Return the Qimage from the cache
+        Return the ``QImage`` from the cache. If not present wait for the
+        background thread to process it.
         """
         log.debug(u'get_image %s' % name)
-        return self._cache[name].image
+        image = self._cache[name]
+        if image.image is None:
+            self._conversion_queue.remove((image.priority, image))
+            image.priority = Priority.High
+            self._conversion_queue.put((image.priority, image))
+            while image.image is None:
+                log.debug(u'get_image - waiting')
+                time.sleep(0.1)
+        return image.image
 
     def get_image_bytes(self, name):
         """
-        Returns the byte string for an image
-        If not present wait for the background thread to process it.
+        Returns the byte string for an image. If not present wait for the
+        background thread to process it.
         """
         log.debug(u'get_image_bytes %s' % name)
-        if not self._cache[name].image_bytes:
-            while self._cache[name].dirty:
+        image = self._cache[name]
+        if image.image_bytes is None:
+            self._conversion_queue.remove((image.priority, image))
+            image.priority = Priority.Urgent
+            self._conversion_queue.put((image.priority, image))
+            while image.image_bytes is None:
                 log.debug(u'get_image_bytes - waiting')
                 time.sleep(0.1)
-        return self._cache[name].image_bytes
+        return image.image_bytes
 
     def del_image(self, name):
         """
-        Delete the Image from the Cache
+        Delete the Image from the cache.
         """
         log.debug(u'del_image %s' % name)
         if name in self._cache:
+            self._conversion_queue.remove(
+                (self._cache[name].priority, self._cache[name]))
             del self._cache[name]
 
     def add_image(self, name, path):
         """
-        Add image to cache if it is not already there
+        Add image to cache if it is not already there.
         """
         log.debug(u'add_image %s:%s' % (name, path))
         if not name in self._cache:
-            image = Image()
-            image.name = name
-            image.path = path
-            image.image = resize_image(path, self.width, self.height)
+            image = Image(name, path)
             self._cache[name] = image
+            self._conversion_queue.put((image.priority, image))
         else:
             log.debug(u'Image in cache %s:%s' % (name, path))
-        self._cache_dirty = True
-        # only one thread please
-        if not self._thread_running:
-            self.image_thread.start()
-
-    def process(self):
-        """
-        Controls the processing called from a QThread
-        """
-        log.debug(u'process - started')
-        self._thread_running = True
-        self.clean_cache()
-        # data loaded since we started ?
-        while self._cache_dirty:
-            log.debug(u'process - recycle')
-            self.clean_cache()
-        self._thread_running = False
-        log.debug(u'process - ended')
-
-    def clean_cache(self):
+        # We want only one thread.
+        if not self._imageThread.isRunning():
+            self._imageThread.start()
+
+    def _process(self):
+        """
+        Controls the processing called from a ``QtCore.QThread``.
+        """
+        log.debug(u'_process - started')
+        while not self._conversion_queue.empty():
+            self._process_cache()
+        log.debug(u'_process - ended')
+
+    def _process_cache(self):
         """
         Actually does the work.
         """
-        log.debug(u'clean_cache')
-        # we will clean the cache now
-        self._cache_dirty = False
-        for key in self._cache.keys():
-            image = self._cache[key]
-            if image.dirty:
-                image.image_bytes = image_to_byte(image.image)
-                image.dirty = False
+        log.debug(u'_process_cache')
+        image = self._conversion_queue.get()[1]
+        # Generate the QImage for the image.
+        if image.image is None:
+            image.image = resize_image(image.path, self.width, self.height)
+            # Set the priority to Lowest and stop here as we need to process
+            # more important images first.
+            if image.priority == Priority.Normal:
+                self._conversion_queue.remove((image.priority, image))
+                image.priority = Priority.Lowest
+                self._conversion_queue.put((image.priority, image))
+                return
+            # For image with high priority we set the priority to Low, as the
+            # byte stream might be needed earlier the byte stream of image with
+            # Normal priority. We stop here as we need to process more important
+            # images first.
+            elif image.priority == Priority.High:
+                self._conversion_queue.remove((image.priority, image))
+                image.priority = Priority.Low
+                self._conversion_queue.put((image.priority, image))
+                return
+        # Generate the byte stream for the image.
+        if image.image_bytes is None:
+            image.image_bytes = image_to_byte(image.image)


Follow ups