[gedit-plugins] Move Ggit.Repository.file_status() call into a worker thread



commit 36ad21861bbd3ed032843cd3f073783143fb147a
Author: Garrett Regier <garrettregier gmail com>
Date:   Sat Jun 28 14:56:47 2014 -0700

    Move Ggit.Repository.file_status() call into a worker thread
    
    Otherwise large repositories can block the gedit UI.

 plugins/git/Makefile.am              |    3 +-
 plugins/git/git/windowactivatable.py |   93 +++++++++++-----------
 plugins/git/git/workerthread.py      |  144 ++++++++++++++++++++++++++++++++++
 3 files changed, 192 insertions(+), 48 deletions(-)
---
diff --git a/plugins/git/Makefile.am b/plugins/git/Makefile.am
index 8602243..94d1eb8 100644
--- a/plugins/git/Makefile.am
+++ b/plugins/git/Makefile.am
@@ -4,7 +4,8 @@ plugins_git_PYTHON =                            \
        plugins/git/git/__init__.py             \
        plugins/git/git/diffrenderer.py         \
        plugins/git/git/viewactivatable.py      \
-       plugins/git/git/windowactivatable.py
+       plugins/git/git/windowactivatable.py    \
+       plugins/git/git/workerthread.py
 
 plugin_in_files += plugins/git/git.plugin.desktop.in
 appstream_in_files += plugins/git/gedit-git.metainfo.xml.in
diff --git a/plugins/git/git/windowactivatable.py b/plugins/git/git/windowactivatable.py
index 674e0cb..85d789f 100644
--- a/plugins/git/git/windowactivatable.py
+++ b/plugins/git/git/windowactivatable.py
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 
-#  Copyright (C) 2013 - Garrett Regier
+#  Copyright (C) 2013-2014 - Garrett Regier
 #
 #  This program is free software; you can redistribute it and/or modify
 #  it under the terms of the GNU General Public License as published by
@@ -21,6 +21,16 @@ from gi.repository import GLib, GObject, Gio, Gedit, Ggit
 
 import weakref
 
+from .workerthread import WorkerThread
+
+
+class GitStatusThread(WorkerThread):
+    def push(self, repo, location):
+        super().push(repo, location)
+
+    def handle_task(self, repo, location):
+        return location, repo.file_status(location)
+
 
 class GitWindowActivatable(GObject.Object, Gedit.WindowActivatable):
     window = GObject.property(type=Gedit.Window)
@@ -34,9 +44,6 @@ class GitWindowActivatable(GObject.Object, Gedit.WindowActivatable):
 
         self.view_activatables = weakref.WeakSet()
 
-        self.files = {}
-        self.monitors = {}
-
         self.repo = None
         self.tree = None
 
@@ -61,6 +68,9 @@ class GitWindowActivatable(GObject.Object, Gedit.WindowActivatable):
 
         self.bus = self.window.get_message_bus()
 
+        self.git_status_thread = GitStatusThread(self.update_location)
+        self.git_status_thread.start()
+
         self.files = {}
         self.file_names = {}
         self.monitors = {}
@@ -83,6 +93,7 @@ class GitWindowActivatable(GObject.Object, Gedit.WindowActivatable):
 
     def do_deactivate(self):
         self.clear_monitors()
+        self.git_status_thread.terminate()
 
         for sid in self.window_signals:
             self.window.disconnect(sid)
@@ -107,7 +118,7 @@ class GitWindowActivatable(GObject.Object, Gedit.WindowActivatable):
         location = view_activatable.view.get_buffer().get_file().get_location()
 
         if location is not None:
-            self.update_location(location)
+            self.git_status_thread.push(self.repo, location)
 
     def tab_removed(self, window, tab):
         view = tab.get_view()
@@ -117,27 +128,24 @@ class GitWindowActivatable(GObject.Object, Gedit.WindowActivatable):
             return
 
         # Need to remove the view activatable otherwise update_location()
-        # will might use the view's status and not the file's actual status
+        # might use the view's status and not the file's actual status
         for view_activatable in self.view_activatables:
             if view_activatable.view == view:
                 self.view_activatables.remove(view_activatable)
                 break
 
-        self.update_location(location)
-
-    def update_location_idle(self, location):
-        self.update_location(location)
-        return False
+        self.git_status_thread.push(self.repo, location)
 
     def focus_in_event(self, window, event):
         for view_activatable in self.view_activatables:
             view_activatable.update()
 
         for uri in self.files:
-            GLib.idle_add(self.update_location_idle, Gio.File.new_for_uri(uri))
+            self.git_status_thread.push(self.repo, Gio.File.new_for_uri(uri))
 
     def root_changed(self, bus, msg, data=None):
         self.clear_monitors()
+        self.git_status_thread.clear()
 
         if not msg.location.has_uri_scheme('file'):
             return
@@ -157,16 +165,19 @@ class GitWindowActivatable(GObject.Object, Gedit.WindowActivatable):
             self.monitor_directory(msg.location)
 
     def inserted(self, bus, msg, data=None):
-        if not msg.location.has_uri_scheme('file'):
+        location = msg.location
+        if not location.has_uri_scheme('file'):
             return
 
-        self.files[msg.location.get_uri()] = msg.id
-        self.file_names[msg.location.get_uri()] = msg.name
+        if msg.is_directory:
+            self.monitor_directory(location)
 
-        GLib.idle_add(self.update_location_idle, msg.location)
+        else:
+            uri = location.get_uri()
+            self.files[uri] = msg.id
+            self.file_names[uri] = msg.name
 
-        if msg.is_directory:
-            self.monitor_directory(msg.location)
+            self.git_status_thread.push(self.repo, location)
 
     def deleted(self, bus, msg, data=None):
         # File browser's deleted signal is broken
@@ -174,43 +185,31 @@ class GitWindowActivatable(GObject.Object, Gedit.WindowActivatable):
 
         uri = msg.location.get_uri()
 
-        del self.files[uri]
-        del self.file_names[uri]
-
         if uri in self.monitors:
             self.monitors[uri].cancel()
             del self.monitors[uri]
 
-    def update_location(self, location):
-        if self.repo is None:
-            return
-
-        if location.get_uri() not in self.files:
-            return
-
-        status = None
-
-        # First get the status from the open documents
-        for view_activatable in self.view_activatables:
-            doc_location = view_activatable.view.get_buffer().get_file().get_location()
+        else:
+            del self.files[uri]
+            del self.file_names[uri]
 
-            if doc_location is not None and doc_location.equal(location):
-                status = view_activatable.status
-                break
+    def update_location(self, result):
+        location, status = result
 
-        try:
-            git_status = self.repo.file_status(location)
+        uri = location.get_uri()
+        if uri not in self.files:
+            return
 
-        except Exception:
-            pass
+        if status is None or not status & Ggit.StatusFlags.IGNORED:
+            for view_activatable in self.view_activatables:
+                view = view_activatable.view
+                doc_location = view.get_buffer().get_file().get_location()
 
-        else:
-            # Don't use the view activatable's
-            # status if the file is ignored
-            if status is None or git_status & Ggit.StatusFlags.IGNORED:
-                status = git_status
+                if doc_location is not None and doc_location.equal(location):
+                    status = view_activatable.status
+                    break
 
-        markup = GLib.markup_escape_text(self.file_names[location.get_uri()])
+        markup = GLib.markup_escape_text(self.file_names[uri])
 
         if status is not None:
             if status & Ggit.StatusFlags.INDEX_NEW or \
@@ -224,7 +223,7 @@ class GitWindowActivatable(GObject.Object, Gedit.WindowActivatable):
                 markup = '<span strikethrough="true">%s</span>' % (markup)
 
         self.bus.send('/plugins/filebrowser', 'set_markup',
-                      id=self.files[location.get_uri()], markup=markup)
+                      id=self.files[uri], markup=markup)
 
     def clear_monitors(self):
         for uri in self.monitors:
diff --git a/plugins/git/git/workerthread.py b/plugins/git/git/workerthread.py
new file mode 100644
index 0000000..07cbea2
--- /dev/null
+++ b/plugins/git/git/workerthread.py
@@ -0,0 +1,144 @@
+# -*- coding: utf-8 -*-
+
+#  Copyright (C) 2014 - Garrett Regier
+#
+#  This program is free software; you can redistribute it and/or modify
+#  it under the terms of the GNU General Public License as published by
+#  the Free Software Foundation; either version 2 of the License, or
+#  (at your option) any later version.
+#
+#  This program is distributed in the hope that it will be useful,
+#  but WITHOUT ANY WARRANTY; without even the implied warranty of
+#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+#  GNU General Public License for more details.
+#
+#  You should have received a copy of the GNU General Public License
+#  along with this program; if not, write to the Free Software
+#  Foundation, Inc., 59 Temple Place, Suite 330,
+#  Boston, MA 02111-1307, USA.
+
+from gi.repository import GLib
+
+import abc
+import collections
+import queue
+import threading
+import traceback
+
+
+class WorkerThread(threading.Thread):
+    __metaclass__ = abc.ABCMeta
+
+    __sentinel = object()
+
+    def __init__(self, callback, chunk_size=1, *args, **kwargs):
+        super().__init__(*args, **kwargs)
+
+        self.__callback = callback
+        self.__chunk_size = chunk_size
+
+        self.__quit = threading.Event()
+        self.__has_idle = threading.Event()
+
+        self.__tasks = queue.Queue()
+        self.__results = collections.deque()
+
+    @abc.abstractmethod
+    def handle_task(self, *args, **kwargs):
+        raise NotImplementedError
+
+    # TODO: add, put, push?
+    def push(self, *args, **kwargs):
+        self.__tasks.put((args, kwargs))
+
+    def __close(self, process_results):
+        self.__quit.set()
+
+        # Prevent the queue.get() from blocking forever
+        self.__tasks.put(self.__sentinel)
+
+        super().join()
+
+        if not process_results:
+            self.__results.clear()
+
+        else:
+            while self.__in_idle() is GLib.SOURCE_CONTINUE:
+                pass
+
+    def terminate(self):
+        self.__close(False)
+
+    def join(self):
+        self.__close(True)
+
+    def clear(self):
+        old_tasks = self.__tasks
+        self.__tasks = queue.Queue(1)
+
+        # Prevent the queue.get() from blocking forever
+        old_tasks.put(self.__sentinel)
+
+        # Block until the old queue has finished, otherwise
+        # a old result could be added to the new results queue
+        self.__tasks.put(self.__sentinel)
+        self.__tasks.put(self.__sentinel)
+
+        old_tasks = self.__tasks
+        self.__tasks = queue.Queue()
+
+        # Switch to the new queue
+        old_tasks.put(self.__sentinel)
+
+        # Finally, we can now create a new deque without
+        # the possibility of any old results being added to it
+        self.__results.clear()
+
+    def run(self):
+        while not self.__quit.is_set():
+            task = self.__tasks.get()
+            if task is self.__sentinel:
+                continue
+
+            args, kwargs = task
+
+            try:
+                result = self.handle_task(*args, **kwargs)
+
+            except Exception:
+                traceback.print_exc()
+                continue
+
+            self.__results.append(result)
+
+            # Avoid having an idle for every result
+            if not self.__has_idle.is_set():
+                self.__has_idle.set()
+
+                GLib.source_set_name_by_id(GLib.idle_add(self.__in_idle),
+                                           '[gedit] %s result callback idle' %
+                                           (self.__class__.__name__))
+
+    def __in_idle(self):
+        try:
+            for i in range(self.__chunk_size):
+                result = self.__results.popleft()
+
+                try:
+                    self.__callback(result)
+
+                except Exception:
+                    traceback.print_exc()
+
+        except IndexError:
+            # Must be cleared before we check the results length
+            self.__has_idle.clear()
+
+            # Only remove the idle when there are no more items,
+            # some could have been added after the IndexError was raised
+            if len(self.__results) == 0:
+                return GLib.SOURCE_REMOVE
+
+        return GLib.SOURCE_CONTINUE
+
+# ex:ts=4:et:


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]