remove multithreading; load only thumbnails to quicken loading
authorgc <gc>
Sat, 23 Feb 2008 15:13:01 +0000 (15:13 +0000)
committergc <gc>
Sat, 23 Feb 2008 15:13:01 +0000 (15:13 +0000)
bin/booh-classifier

index c2a17c3b5cbed3c29f4058f904bc250406e6d9de..ff6d7af37a4894896ae43d2331cbb32b69d6ca99 100644 (file)
@@ -21,7 +21,6 @@
 
 require 'getoptlong'
 require 'tempfile'
-require 'thread'
 
 require 'gtk2'
 require 'booh/libadds'
@@ -46,6 +45,8 @@ $options = [
     [ '--sort-by-exif-date', '-s', GetoptLong::NO_ARGUMENT, _("Sort entries by EXIF date") ],
 ]
 
+$preloader_allowed = false
+
 def usage
     puts _("Usage: %s [OPTION]...") % File.basename($0)
     $options.each { |ary|
@@ -244,86 +245,97 @@ class Entry
         if @@max_width.nil?
             @@max_width = $main_window.root_window.size[0] - $labels_vbox.allocation.width - ( $videoborder_pixbuf.width + MainView.borders_thickness) * 2
         end
-        @protect_cleanup = Mutex.new
     end
 
     def pixbuf_full
-        @protect_cleanup.synchronize {
-            if @pixbuf_full.nil?
-                msg 3, ">>> pixbuf_full #{path}"
-                load_into_pixbuf_full
-            end
-            return @pixbuf_full
-        }
+        if @pixbuf_full.nil?
+            msg 3, ">>> pixbuf_full #{path}"
+            load_into_pixbuf_full
+        end
+        return @pixbuf_full
     end
     def free_pixbuf_full
-        @protect_cleanup.synchronize {
+        if @pixbuf_full.nil?
+            return false
+        else
+            msg 3, ">>> free_pixbuf_full #{path}"
+            @pixbuf_full = nil
+            return true
+        end
+    end
+    def pixbuf_main
+        width, height = $mainview.window.size 
+        width = MainView.get_usable_width(width)
+        height = MainView.get_usable_height(height)
+        if @pixbuf_main.nil? || width != @width || height != @height
+            msg 3, ">>> pixbuf_main #{path}"
+            @width = width
+            @height = height
+            load_into_pixbuf_full  #- make sure it is loaded
             if @pixbuf_full.nil?
-                return false
-            else
-                msg 3, ">>> free_pixbuf_full #{path}"
-                @pixbuf_full = nil
-                return true
+                return
             end
-        }
-    end
-    def pixbuf_main(width, height)
-        @protect_cleanup.synchronize {
-            if @pixbuf_main.nil? || width != @width || height != @height
-                msg 3, ">>> pixbuf_main #{path}"
-                @width = width
-                @height = height
-                load_into_pixbuf_full  #- make sure it is loaded
-                if @pixbuf_full.width.to_f / @pixbuf_full.height > width.to_f / height
-                    resized_height = @pixbuf_full.height * (width.to_f/@pixbuf_full.width)
-                    if @pixbuf_full.width > width || @pixbuf_full.height > resized_height
-                        @pixbuf_main = @pixbuf_full.scale(width, resized_height, Gdk::Pixbuf::INTERP_BILINEAR)
-                    else
-                        @pixbuf_main = @pixbuf_full
-                    end
+            if @pixbuf_full.width.to_f / @pixbuf_full.height > width.to_f / height
+                resized_height = @pixbuf_full.height * (width.to_f/@pixbuf_full.width)
+                if @pixbuf_full.width > width || @pixbuf_full.height > resized_height
+                    @pixbuf_main = @pixbuf_full.scale(width, resized_height, Gdk::Pixbuf::INTERP_BILINEAR)
                 else
-                    resized_width = @pixbuf_full.width * (height.to_f/@pixbuf_full.height)
-                    if @pixbuf_full.width > resized_width || @pixbuf_full.height > height
-                        @pixbuf_main = @pixbuf_full.scale(resized_width, height, Gdk::Pixbuf::INTERP_BILINEAR)
-                    else
-                        @pixbuf_main = @pixbuf_full
-                    end
+                    @pixbuf_main = @pixbuf_full
+                end
+            else
+                resized_width = @pixbuf_full.width * (height.to_f/@pixbuf_full.height)
+                if @pixbuf_full.width > resized_width || @pixbuf_full.height > height
+                    @pixbuf_main = @pixbuf_full.scale(resized_width, height, Gdk::Pixbuf::INTERP_BILINEAR)
+                else
+                    @pixbuf_main = @pixbuf_full
                 end
             end
-            return @pixbuf_main
-        }
+        end
+        return @pixbuf_main
     end
     def free_pixbuf_main
-        @protect_cleanup.synchronize {
-            if @pixbuf_main.nil?
-                return false
-            else
-                msg 3, ">>> free_pixbuf_main #{path}"
-                @pixbuf_main = nil
-                return true
-            end
-        }
+        if @pixbuf_main.nil?
+            return false
+        else
+            msg 3, ">>> free_pixbuf_main #{path}"
+            @pixbuf_main = nil
+            return true
+        end
     end
     def pixbuf_thumbnail
-        return @protect_cleanup.synchronize {
-            if @pixbuf_thumbnail.nil?
-                msg 3, ">>> pixbuf_thumbnail #{path}"
-                load_into_pixbuf_full  #- make sure it is loaded
-                @pixbuf_thumbnail = @pixbuf_full.scale(@pixbuf_full.width * (@@thumbnails_height.to_f/@pixbuf_full.height), @@thumbnails_height, Gdk::Pixbuf::INTERP_BILINEAR)
+        if @pixbuf_thumbnail.nil?
+            if @pixbuf_main
+                msg 3, ">>> pixbuf_thumbnail from main #{path}"
+                @pixbuf_thumbnail = @pixbuf_main.scale(@pixbuf_main.width * (@@thumbnails_height.to_f/@pixbuf_main.height), @@thumbnails_height, Gdk::Pixbuf::INTERP_BILINEAR)
+            else
+                msg 3, ">>> pixbuf_thumbnail from file #{path}"
+                @pixbuf_thumbnail = load_into_pixbuf_at_size { |w, h|
+                    if @angle == 0
+                        if h > @@thumbnails_height
+                            [ w * @@thumbnails_height.to_f/h, @@thumbnails_height ]
+                        else
+                            [ w, h ]
+                        end
+                    else
+                        if w > @@thumbnails_height
+                            [ @@thumbnails_height, h * @@thumbnails_height.to_f/w ]
+                        else
+                            [ w, h ]
+                        end
+                    end
+                }
             end
-            return @pixbuf_thumbnail
-        }
+        end
+        return @pixbuf_thumbnail
     end
     def free_pixbuf_thumbnail
-        @protect_cleanup.synchronize {
-            if @pixbuf_thumbnail.nil?
-                return false
-            else
-                msg 3, ">>> free_pixbuf_thumbnail #{path}"
-                @pixbuf_thumbnail = nil
-                return true
-            end
-        }
+        if @pixbuf_thumbnail.nil?
+            return false
+        else
+            msg 3, ">>> free_pixbuf_thumbnail #{path}"
+            @pixbuf_thumbnail = nil
+            return true
+        end
     end
 
     def outline_color
@@ -370,66 +382,86 @@ class Entry
     def load_into_pixbuf_full
         if @pixbuf_full.nil?
             msg 3, ">>> load_into_pixbuf_full #{path}"
-            if @type == 'video'
-                tmp = Tempfile.new("boohclassifiertemp")
-                tmp.close!
-                Dir.mkdir(dest_dir = tmp.path)
-                orig_base = File.basename(path)
-                tmpdir = gen_video_thumbnail(path, false, 0)
-                if tmpdir.nil?
-                    return
-                end
-                image_path = "#{tmpdir}/00000001.jpg"
-            else
-                image_path = @path
-            end
-            begin
-                #- use a pixbuf loader to allow the loading thread to be interrupted by the main thread more often, to keep the UI responsive even
-                #- if loaded pictures are several MBs large
-                loader = Gdk::PixbufLoader.new
-                loader.signal_connect('size-prepared') { |l,w,h|
-                    maxdim = w > h ? w : h
-                    if maxdim > @@max_width
+            @pixbuf_full = load_into_pixbuf_at_size { |w, h|
+                if @angle == 0
+                    if w > @@max_width
                         #- save memory and speedup (+35%) loading 
-                        loader.set_size(w * (factor = @@max_width.to_f/maxdim), h * factor)
+                        [ w * (factor = @@max_width.to_f/w), h * factor ]
+                    else
+                        [ w, h ]
                     end
-                }
-                loader.signal_connect('area-prepared') { @pixbuf_full = loader.pixbuf }
-                file = File.new(image_path)
-                while (chunk = file.read(65536)) != nil
-                    loader.write(chunk)
-                end
-                file.close
-                loader.close
-                if @pixbuf_full.nil?
-                    raise "Loaded pixbuf nil - #{path} #{image_path}"
-                end
-            rescue Gdk::PixbufError
-                msg 0, "Cannot load #{image_path}: #{$!}"
-                return
-            ensure
-                if @type == 'video'
-                    File.delete(image_path)
-                    Dir.rmdir(tmpdir)
-                end
-            end
-            if @pixbuf_full
-                if @angle.nil?
-                    if @type == 'image'
-                        @angle = guess_rotate(image_path)
+                else
+                    if h > @@max_width
+                        [ w * (factor = @@max_width.to_f/h), h * factor ]
                     else
-                        @angle = 0
+                        [ w, h ]
                     end
                 end
-                if @angle != 0
-                    msg 3, ">>> load_into_pixbuf_full #{image_path} => rotate #{@angle}"
-                    @pixbuf_full = rotate_pixbuf(@pixbuf_full, @angle)
-                end
+            }
+        end
+    end
+
+    def load_into_pixbuf_at_size(&specify_size)
+        pixbuf = nil
+        if @type == 'video'
+            tmp = Tempfile.new("boohclassifiertemp")
+            tmp.close!
+            Dir.mkdir(dest_dir = tmp.path)
+            orig_base = File.basename(path)
+            tmpdir = gen_video_thumbnail(path, false, 0)
+            if tmpdir.nil?
+                return
+            end
+            image_path = "#{tmpdir}/00000001.jpg"
+        else
+            image_path = @path
+        end
+        if @angle.nil?
+            if @type == 'image'
+                @angle = guess_rotate(image_path)
+            else
+                @angle = 0
             end
+        end
+        begin
+            #- use a pixbuf loader and trigger Gtk.main_iteration on each chunk if needed, to keep the UI responsive even
+            #- if loaded pictures are several MBs large
+            loader = Gdk::PixbufLoader.new
+            loader.signal_connect('size-prepared') { |l, w, h|
+                r = specify_size.call(w, h)
+                msg 3, "specified sizes: #{r[0]} #{r[1]}"
+                loader.set_size(*specify_size.call(w, h))
+            }
+            loader.signal_connect('area-prepared') { pixbuf = loader.pixbuf }
+            file = File.new(image_path)
+            while (chunk = file.read(4096)) != nil
+                loader.write(chunk)
+                Gtk.main_iteration while Gtk.events_pending?
+            end
+            file.close
+            loader.close
+            if pixbuf.nil?
+                raise "Loaded pixbuf nil - #{path} #{image_path}"
+            end
+        rescue Gdk::PixbufError
+            msg 0, "Cannot load #{image_path}: #{$!}"
+            return
+        ensure
             if @type == 'video'
-                cleanup_dir(dest_dir)
+                File.delete(image_path)
+                Dir.rmdir(tmpdir)
             end
         end
+        if pixbuf
+            if @angle != 0
+                msg 3, ">>> load_into_pixbuf_full #{image_path} => rotate #{@angle}"
+                pixbuf = rotate_pixbuf(pixbuf, @angle)
+            end
+        end
+        if @type == 'video'
+            cleanup_dir(dest_dir)
+        end
+        return pixbuf
     end
 
     def to_s
@@ -439,6 +471,54 @@ end
 
 $allentries = []
 
+def run_preloader_real
+    msg 3, "*** >> main preloading triggered..."
+    if $preloader_running
+        msg 3, "*** >>>>>> already running, return <<<<<<<<"
+        return
+    end
+    $preloader_running = true
+    if $mainview.get_shown_entry
+        mem = get_mem
+        if mem > $config['cache-memory-use-figure']
+            msg 3, "too much RSS, stopping preloading, triggering GC"
+            $preloader_running = false
+            GC.start
+            msg 3, "GC finished"
+            return
+        end
+        index = $allentries.index($mainview.get_shown_entry)
+        for j in 1 .. $config['preload-distance'].to_i
+            i = index + j
+            if i < $allentries.size
+                $allentries[i].pixbuf_main
+            end
+            i = index - j
+            if i >= 0
+                $allentries[i].pixbuf_main
+            end
+        end
+        check_memory_free_cache_if_needed
+    end
+    $preloader_running = false
+    msg 3, "*** << main preloading finished"
+    #- if we're already on a different image, rerun the preloader
+    if index != $allentries.index($mainview.get_shown_entry)
+        run_preloader_real
+    end
+end
+
+def run_preloader
+    if ! $preloader_allowed
+        msg 3, "*** preloader not yet allowed"
+        return
+    end
+    Gtk.timeout_add(10) {
+        run_preloader_real
+        false
+    }
+end
+
 class MainView < Gtk::DrawingArea
 
     @@borders_thickness = 5
@@ -459,44 +539,12 @@ class MainView < Gtk::DrawingArea
     def initialize
         super()
         signal_connect('expose-event') { draw }
-        signal_connect('configure-event') { update_shown; GC.start }
-        signal_connect('realize') {
-            @preloader = Thread.new {
-                #- background preloading
-                while true
-                    msg 3, "*** >> background main preloading triggered..."
-                    if ! @entry.nil?
-                        index = $allentries.index(@entry)
-                        w, h = window.size
-                        w = MainView.get_usable_width(w)
-                        h = MainView.get_usable_height(h)
-                        for j in 1 .. $config['preload-distance'].to_i
-                            i = index + j
-                            if i < $allentries.size
-                                $allentries[i].pixbuf_main(w, h)
-                            end
-                            i = index - j
-                            if i >= 0
-                                $allentries[i].pixbuf_main(w, h)
-                            end
-                            GC.start
-                            mem = get_mem
-                            if mem > $config['cache-memory-use-figure']
-                                msg 3, "too much RSS, stopping main preloading"
-                                break
-                            end
-                        end
-                        check_memory_free_cache_if_needed
-                    end
-                    msg 3, "*** << background main preloading stopping"
-                    Thread.stop
-                end
-            }
-            @preloader.priority = -2
-        }
+        signal_connect('configure-event') { update_shown }
+        @preloader_running = false
     end
 
     def set_shown_entry(entry)
+        t1 = Time.now
         if ! entry.nil? && entry == @entry
             return
         end
@@ -510,7 +558,8 @@ class MainView < Gtk::DrawingArea
         end
         @entry = entry
         redraw        
-        @preloader.run
+        run_preloader
+        msg 3, "entry shown in: #{Time.now - t1} s"
     end
 
     def get_shown_entry
@@ -535,13 +584,10 @@ class MainView < Gtk::DrawingArea
 
     def update_shown
         if @entry
+            @pixbuf = @entry.pixbuf_main
             width, height = window.size 
-            usable_width = MainView.get_usable_width(width)
-            usable_height = MainView.get_usable_height(height)
-            @pixbuf = @entry.pixbuf_main(usable_width, usable_height)
             @xpos = (width - @pixbuf.width)/2
             @ypos = (height - @pixbuf.height)/2
-            @preloader.run
         else
             @pixbuf = nil
         end
@@ -580,28 +626,25 @@ class MainView < Gtk::DrawingArea
 end
 
 def check_memory_free_cache_if_needed
-    GC.start
-    mem = get_mem
     i = $allentries.index($mainview.get_shown_entry)
-    msg 3, "mem: #{mem} index: #{i}"
     return if i.nil?
+    if get_mem < $config['cache-memory-use-figure'] * 2 / 3
+        return
+    end
+    msg 3, "too much RSS, triggering GC"
+    GC.start
+    msg 3, "GC finished"
     ($allentries.size - 1).downto(1) { |j|
-        if mem < $config['cache-memory-use-figure'] * 2 / 3
+        if get_mem < $config['cache-memory-use-figure'] / 2
             break
         end
         index = i + j
         msg 3, "too much RSS, freeing full size of #{i+j} and #{i-j}..."
-        freedsomething = false
         if i + j < $allentries.size
-            freedsomething |= $allentries[i+j].free_pixbuf_full
+            $allentries[i+j].free_pixbuf_full
         end
         if i - j > 0
-            freedsomething |= $allentries[i-j].free_pixbuf_full
-        end
-        if freedsomething
-            GC.start
-            mem = get_mem
-            msg 3, "\tmem now: #{mem}"
+            $allentries[i-j].free_pixbuf_full
         end
     }
 end
@@ -885,75 +928,86 @@ def sb_msg(msg)
     end
 end
 
-def real_show_entry(entry, tooltips, grab_focus)
-    #- for scoping the actual reference to 'entry'
-    gtk_thread_protect(proc {
-        msg 3, "using entry #{entry}"
-        entry.image = Gtk::Image.new(entry.pixbuf_thumbnail)
-        if entry.type == 'video'
-            entry.button = Gtk::Button.new.add(Gtk::HBox.new.pack_start(da1 = Gtk::DrawingArea.new.set_size_request($videoborder_pixbuf.width, -1), false, false).
-                                                             pack_start(entry.image).
-                                                             pack_start(da2 = Gtk::DrawingArea.new.set_size_request($videoborder_pixbuf.width, -1), false, false))
-            da1.signal_connect('realize') { da1.window.set_back_pixmap($videoborder_pixmap, false) }
-            da2.signal_connect('realize') { da2.window.set_back_pixmap($videoborder_pixmap, false) }
-        else
-            entry.button = Gtk::Button.new.set_image(entry.image)
+def show_entry(entry, i)
+    #- scope entry
+    msg 3, "showing entry #{entry}"
+    entry.image = Gtk::Image.new(entry.pixbuf_thumbnail)
+    if entry.type == 'video'
+        entry.button = Gtk::Button.new.add(Gtk::HBox.new.pack_start(da1 = Gtk::DrawingArea.new.set_size_request($videoborder_pixbuf.width, -1), false, false).
+                                           pack_start(entry.image).
+                                           pack_start(da2 = Gtk::DrawingArea.new.set_size_request($videoborder_pixbuf.width, -1), false, false))
+        da1.signal_connect('realize') { da1.window.set_back_pixmap($videoborder_pixmap, false) }
+        da2.signal_connect('realize') { da2.window.set_back_pixmap($videoborder_pixmap, false) }
+    else
+        entry.button = Gtk::Button.new.set_image(entry.image)
+    end
+    Gtk::Tooltips.new.set_tip(entry.button, entry.get_beautified_name, nil)
+    $imagesline.pack_start(entry.alignment = Gtk::Alignment.new(0.5, 1, 0, 0).add(entry.button).show_all, false, false)
+    entry.button.signal_connect('clicked') {
+        if (last_shown = $mainview.get_shown_entry) != entry
+            $mainview.set_shown_entry(entry)
+            entry.alignment.set(0.5, 0, 0, 0)
+            sb_msg(_("Selected %s") % entry.get_beautified_name)
+            last_shown and last_shown.alignment.set(0.5, 1, 0, 0)
         end
-        tooltips.set_tip(entry.button, entry.get_beautified_name, nil)
-        $imagesline.pack_start(entry.alignment = Gtk::Alignment.new(0.5, 1, 0, 0).add(entry.button).show_all, false, false)
-        entry.button.signal_connect('clicked') {
-            if (last_shown = $mainview.get_shown_entry) != entry
-                $mainview.set_shown_entry(entry)
-                entry.alignment.set(0.5, 0, 0, 0)
-                sb_msg(_("Selected %s") % entry.get_beautified_name)
-                last_shown.nil? or last_shown.alignment.set(0.5, 1, 0, 0)
-            end
-        }
-        entry.button.signal_connect('button-press-event') { |w, event|
-            if entry.type == 'video' && event.event_type == Gdk::Event::BUTTON2_PRESS
-                video_view(entry)
-            end
-        }
-        entry.button.signal_connect('focus-in-event') { entry.button.clicked; autoscroll_if_needed(entry.button) }
-        entry.button.signal_connect('key-press-event') { |w, e| thumbnail_keypressed(entry, e) }
-        if grab_focus
-            entry.button.grab_focus
+    }
+    entry.button.signal_connect('button-press-event') { |w, event|
+        if entry.type == 'video' && event.event_type == Gdk::Event::BUTTON2_PRESS
+            video_view(entry)
         end
-        update_visibility(entry)
-    })
+    }
+    entry.button.signal_connect('focus-in-event') { entry.button.clicked; autoscroll_if_needed(entry.button) }
+    entry.button.signal_connect('key-press-event') { |w, e| thumbnail_keypressed(entry, e) }
+    if i == 0
+        entry.button.grab_focus
+    end
+    update_visibility(entry)
+    Gtk.main_iteration while Gtk.events_pending?
 end
 
 def show_entries
-    $images_loader = Thread.new {
-        t1 = Time.now
-        sb_msg(_("Loading images..."))
-        tooltips = Gtk::Tooltips.new
-        counter = 0
-        toremove = []
-        grab_focus = true
-        i = 0
-        total_size = 0
-        while i < $allentries.size
-            entry = $allentries[i]
-            if entry.pixbuf_full
-                total_size += file_size(entry.path)
-                entry.pixbuf_thumbnail
-                real_show_entry(entry, tooltips, grab_focus)
-                grab_focus = false
-                if i % 4 == 0
-                    check_memory_free_cache_if_needed
-                end
-                counter += 1
-                i = i + 1
+    sb_msg(_("Loading images..."))
+    t1 = Time.now
+    total_loaded_files = 0
+    total_loaded_size = 0
+    i = 0
+    while i < $allentries.size
+#        printf "%d %s\n", i, __LINE__
+        entry = $allentries[i]
+        if i == 0
+            loaded_pixbuf = entry.pixbuf_main
+        else
+            loaded_pixbuf = entry.pixbuf_thumbnail
+        end
 
-            else
-                $allentries.delete_at(i)
+        if loaded_pixbuf
+            show_entry(entry, i)
+
+            total_loaded_size += file_size(entry.path)
+            if i % 4 == 0
+                check_memory_free_cache_if_needed
             end
+            total_loaded_files += 1
+            i += 1
+            if i > $config['preload-distance'].to_i && i <= $config['preload-distance'].to_i * 2
+                #- when we're at preload distance, beging preloading to preload distance
+                $allentries[i - $config['preload-distance'].to_i].pixbuf_main
+            end
+            if i == $config['preload-distance'].to_i * 2 + 1
+                #- when we're after double preload distance, activate normal preloading
+                $preloader_allowed = true
+            end
+            
+        else
+            $allentries.delete_at(i)
         end
-        check_memory_free_cache_if_needed
-        sb_msg(_("%d images of total %s kB loaded in %3.2f seconds.") % [ counter, commify(total_size / 1024), Time.now - t1 ])
-    }
-    $images_loader.priority = -1
+    end
+    if i <= $config['preload-distance'].to_i * 2
+        #- not yet preloaded correctly
+        $preloader_allowed = true
+        run_preloader
+    end
+    sb_msg(_("%d images of total %s kB loaded in %3.2f seconds.") % [ total_loaded_files, commify(total_loaded_size / 1024), Time.now - t1 ])
 end
 
 def reset_all
@@ -1042,25 +1096,6 @@ def open_dir_popup
     fc.destroy
 end
 
-def gtk_thread_protect(proc, *params)
-    if Thread.current == Thread.main
-        proc.call(*params)
-    else
-        $protect_gtk_pending_calls.synchronize {
-            $gtk_pending_calls << [ proc, params ]
-        }
-    end
-end
-
-def gtk_thread_flush
-    $protect_gtk_pending_calls.synchronize {
-        if $gtk_pending_calls.size > 0
-            elem = $gtk_pending_calls.shift
-            elem[0].call(*elem[1])
-        end
-    }
-end
-
 def try_quit(*options)
     Gtk.main_quit
 end
@@ -1548,19 +1583,11 @@ def create_main_window
         false
     }
 
-    $protect_gtk_pending_calls = Mutex.new
-    $gtk_pending_calls = []
-    Gtk.timeout_add(50) {
-        gtk_thread_flush
-        true
-    }
-
     $main_window.show_all
 end
 
 
 handle_options
-Thread.abort_on_exception = true
 read_config
 Gtk.init