-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #5 from compas-dev/background-worker
Background task component
- Loading branch information
Showing
21 changed files
with
475 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,3 +8,5 @@ API Reference | |
api/compas_eve | ||
api/compas_eve.mqtt | ||
api/compas_eve.memory | ||
api/compas_eve.ghpython | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
|
||
.. automodule:: compas_eve.ghpython |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,76 @@ | ||
******************************************************************************** | ||
Examples in Rhino/Grasshopper | ||
******************************************************************************** | ||
|
||
.. currentmodule:: compas_eve | ||
|
||
.. note:: | ||
|
||
This tutorial assumes that you have already installed ``compas_eve``. | ||
If you haven't, please follow the instructions in the :ref:`installation` section. | ||
|
||
**COMPAS EVE** provides tools to work with events inside Rhino/Grasshopper, as well as | ||
the ability to run long-running tasks in the background, which would otherwise block the UI. | ||
|
||
Long-running tasks | ||
------------------ | ||
|
||
A long-running task is any snippet of code that takes a long time to execute. Normally, this would | ||
freeze the Grasshopper user interface. **COMPAS EVE** provides a mechanism to run such tasks in the | ||
background, so that the user can continue working with Grasshopper while the task is running. | ||
|
||
In order to use it, add a ``Background task`` component to your Grasshopper definition, and connect | ||
an input with a python function containing the code that needs to run in the background. The only | ||
requirement is that this function must accept a ``worker`` argument, which is an instance of | ||
:class:`~compas_eve.ghpython.BackgroundWorker`. | ||
|
||
.. figure:: /_images/background-task.png | ||
:figclass: figure | ||
:class: figure-img img-fluid | ||
|
||
The following code exemplifies how to use it to create a simple background task that generates | ||
a list of random values. The function adds some delay to simulate a long-running task. | ||
|
||
.. code-block:: python | ||
import time | ||
import random | ||
def do_something(worker): | ||
result = [] | ||
for i in range(100): | ||
result.append(random.random()) | ||
worker.display_progress(len(result) / 100) | ||
time.sleep(0.05) | ||
worker.display_message("Done!") | ||
return result | ||
It is also possible to update the results during the execution of the task. The result | ||
can be of any type, in the previous example it was a list of numbers. | ||
|
||
In the following example, the code generates a list of randomly placed Rhino points | ||
and continuously updates the results as the list grows. The points will appear | ||
in the Rhino Viewport even before the task has completed. | ||
|
||
.. code-block:: python | ||
import time | ||
import random | ||
import Rhino.Geometry as rg | ||
def do_something(worker): | ||
result = [] | ||
for i in range(100): | ||
x, y = random.randint(0, 100), random.randint(0, 100) | ||
result.append(rg.Point3d(x, y, 0)) | ||
worker.update_result(result) | ||
time.sleep(0.01) | ||
worker.display_message("Done!") | ||
return result |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,2 @@ | ||
compas | ||
compas>=1.17.6 | ||
paho-mqtt |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
""" | ||
******************************************************************************** | ||
compas_eve.ghpython | ||
******************************************************************************** | ||
.. currentmodule:: compas_eve.ghpython | ||
Classes | ||
======= | ||
.. autosummary:: | ||
:toctree: generated/ | ||
:nosignatures: | ||
BackgroundWorker | ||
""" | ||
from .background import BackgroundWorker | ||
|
||
__all__ = ["BackgroundWorker"] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,211 @@ | ||
from __future__ import absolute_import | ||
from __future__ import division | ||
from __future__ import print_function | ||
|
||
import threading | ||
|
||
import Rhino | ||
import scriptcontext | ||
import System | ||
from compas_ghpython import create_id | ||
from compas_ghpython import update_component | ||
|
||
|
||
class BackgroundWorker(object): | ||
"""Background worker simplifies the creation of long-running tasks inside Grasshopper. | ||
A long-running task is any piece of code that will run for an extended period of time, | ||
for example, a very complex calculation, or loading a very large data file, etc. | ||
To use it, write your long-running function in a Grasshopper GHPython component, | ||
and pass it as the input to the background worker component. | ||
The worker will continue working without blocking the UI. | ||
The following is an example of a long-running function that updates the | ||
progress while it runs. | ||
.. code-block:: python | ||
import time | ||
def do_something_long_and_complicated(worker): | ||
# Result can be of any data type | ||
result = 0 | ||
for i in range(50): | ||
worker.current_value = i | ||
result += i | ||
worker.display_progress(i / (50-1)) | ||
time.sleep(0.01) | ||
worker.display_message("Done!") | ||
return result | ||
Parameters | ||
---------- | ||
ghenv : ``GhPython.Component.PythonEnvironment`` | ||
Grasshopper environment object | ||
long_running_function : function, optional | ||
This function will be the main entry point for the long-running task. | ||
""" | ||
|
||
def __init__(self, ghenv, long_running_function=None): | ||
super(BackgroundWorker, self).__init__() | ||
self.ghenv = ghenv | ||
self._is_working = False | ||
self._is_done = False | ||
self._is_cancelled = False | ||
self._has_requested_cancellation = False | ||
self.long_running_function = long_running_function | ||
|
||
def is_working(self): | ||
"""Indicate whether the worker is currently working or not.""" | ||
return self._is_working | ||
|
||
def is_done(self): | ||
"""Indicate whether the worker is done or not.""" | ||
return self._is_done | ||
|
||
def has_requested_cancellation(self): | ||
return self._has_requested_cancellation | ||
|
||
def request_cancellation(self): | ||
"""Mark the current worker as cancelled, so that the background task can stop processing.""" | ||
self._has_requested_cancellation = True | ||
|
||
def start_work(self): | ||
"""Start the background processing thread where work will be performed.""" | ||
|
||
def _long_running_task_wrapper(worker): | ||
try: | ||
worker.set_internal_state_to_working() | ||
result = self.long_running_function(self) | ||
worker.set_internal_state_to_done(result) | ||
except Exception as e: | ||
worker.display_message(str(e)) | ||
worker.set_internal_state_to_cancelled() | ||
|
||
target = _long_running_task_wrapper | ||
args = (self,) | ||
self.thread = threading.Thread(target=target, args=args) | ||
self.thread.start() | ||
|
||
def set_internal_state_to_working(self): | ||
"""Set the internal state to ``working``.""" | ||
self._is_working = True | ||
self._is_done = False | ||
self._is_cancelled = False | ||
|
||
def set_internal_state_to_done(self, result): | ||
"""Set the internal state to ``done``, which indicates the worker has completed.""" | ||
self._is_working = False | ||
self._is_done = True | ||
self._is_cancelled = False | ||
self.update_result(result, delay=1) | ||
|
||
def update_result(self, result, delay=1): | ||
"""Update the result of the worker. | ||
This will update the result of the worker, and trigger a solution expiration | ||
of the Grasshopper component. | ||
Parameters | ||
---------- | ||
result : object | ||
Result of the worker. | ||
delay : int, optional | ||
Delay (in milliseconds) before updating the component, by default 1. | ||
""" | ||
self.result = result | ||
update_component(self.ghenv, delay) | ||
|
||
def set_internal_state_to_cancelled(self): | ||
"""Set the internal state to ``cancelled``.""" | ||
self._is_working = False | ||
self._is_done = False | ||
self._is_cancelled = True | ||
|
||
def display_progress(self, progress): | ||
"""Display a progress indicator in the component. | ||
Parameters | ||
---------- | ||
progress : float | ||
Float between ``0..1`` indicating progress of completion. | ||
""" | ||
self.display_message("Progress {:.1f}%".format(progress * 100)) | ||
|
||
def display_message(self, message): | ||
"""Display a message in the component without triggering a solution expiration. | ||
Parameters | ||
---------- | ||
message : str | ||
Message to display. | ||
""" | ||
|
||
def ui_callback(): | ||
self.ghenv.Component.Message = message | ||
self.ghenv.Component.OnDisplayExpired(True) | ||
|
||
Rhino.RhinoApp.InvokeOnUiThread(System.Action(ui_callback)) | ||
|
||
@classmethod | ||
def instance_by_component(cls, ghenv, long_running_function=None, force_new=False): | ||
"""Get the worker instance assigned to the component. | ||
This will get a persistant instance of a background worker | ||
for a given component. The parameter `force_new` can | ||
be set to `True` to request a new instance to be created. | ||
Parameters | ||
---------- | ||
ghenv : ``GhPython.Component.PythonEnvironment`` | ||
Grasshopper environment object | ||
long_running_function : function, optional | ||
This function will be the main entry point for the long-running task. | ||
force_new : bool, optional | ||
Force the creation of a new background worker, by default False. | ||
Returns | ||
------- | ||
:class:`BackgroundWorker` | ||
Instance of the background worker of the current component. | ||
""" | ||
|
||
key = create_id(ghenv.Component, "background_worker") | ||
worker = scriptcontext.sticky.get(key) | ||
|
||
if worker and force_new: | ||
worker.request_cancellation() | ||
worker = None | ||
del scriptcontext.sticky[key] | ||
|
||
if not worker: | ||
worker = cls(ghenv, long_running_function=long_running_function) | ||
scriptcontext.sticky[key] = worker | ||
|
||
return worker | ||
|
||
@classmethod | ||
def stop_instance_by_component(cls, ghenv): | ||
"""Stops the worker instance assigned to the component. | ||
If there is no worker running, it will do nothing. | ||
Parameters | ||
---------- | ||
ghenv : ``GhPython.Component.PythonEnvironment`` | ||
Grasshopper environment object | ||
""" | ||
|
||
key = create_id(ghenv.Component, "background_worker") | ||
worker = scriptcontext.sticky.get(key) | ||
|
||
if worker: | ||
worker.request_cancellation() | ||
worker = None | ||
del scriptcontext.sticky[key] |
Oops, something went wrong.