[gedit-plugins] Move Ggit.Repository.file_status() call into a worker thread
- From: Garrett Regier <gregier src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [gedit-plugins] Move Ggit.Repository.file_status() call into a worker thread
- Date: Sun, 17 Aug 2014 22:44:02 +0000 (UTC)
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]