Module sverchok.core.tasks
Expand source code
import gc
from time import time
from functools import partial, cached_property, cache
from typing import TYPE_CHECKING, Optional, Generator
import bpy
from sverchok.data_structure import post_load_call
from sverchok.core.sv_custom_exceptions import CancelError
from sverchok.utils.sv_logging import catch_log_error, sv_logger
from sverchok.utils.profile import profile
from sverchok.utils.handle_blender_data import BlTrees
if TYPE_CHECKING:
from sverchok.node_tree import SverchCustomTree as SvTree
class Tasks:
"""
It keeps tasks which should be executed and executes the on demand.
1. Execute tasks
2. Time the whole execution
3. Display the progress in the UI
"""
_todo: set['Task']
_current: Optional['Task']
def __init__(self):
""":_todo: list of tasks to run
:_current: task which was started to execute"""
self._todo = set()
self._current = None
def __bool__(self):
"""Has anything to do?"""
return bool(self._current or self._todo)
def add(self, task: 'Task'):
"""Add new tasks to run them via timer"""
if task.is_scene_update:
# scene event can be excepted only as first event
# in all other cases it can be generated by tree evaluation
# and can lead to infinite loop of updates
if self._current is not None:
return
# print(f"Add {task=}")
self._todo.add(task)
@profile(section="UPDATE")
def run(self):
"""Run given tasks to update trees and report execution process in the
header of a node tree editor"""
# 0.15 is max timer frequency
max_duration = 10 if self.current.is_scene_update else 0.15
duration = 0
while self.current:
if duration > max_duration:
return
# print(f"Run task: {self.current}")
duration += self.current.run(max_duration-duration)
if self.current.last_node:
msg = f'Pres "ESC" to abort, updating node "{self.current.last_node.name}"'
self._report_progress(msg)
if self.current.is_exhausted:
self._next()
self._finish()
def cancel(self):
"""Remove all tasks in the queue and abort current one"""
self._todo.clear()
if self._current:
try:
self._current.throw(CancelError)
except (StopIteration, RuntimeError):
pass
finally: # protection from the task to be stack forever
self._finish()
@property
def current(self) -> Optional['Task']:
"""Return current task if it is absent it tries to pop it from the tasks
queue if it's empty returns None"""
if self._current:
return self._current
elif self._todo:
self._start()
self._current = self._todo.pop()
return self._current
else:
return None
def _start(self):
"""Preprocessing before executing the whole queue of events"""
self._start_time
gc.disable() # for performance
def _next(self):
"""Should be called to switch to next tasks when current is exhausted
It made some cleanups after the previous task"""
self._report_progress()
self._current = self._todo.pop() if self._todo else None
del self._main_area
def _finish(self):
"""Cleanups. Also triggers scene handler and mark trees to skip it"""
self._report_progress()
del self._main_area
# this only need to trigger scene changes handler again
# todo should be proved that this is right location to call from
bpy.context.scene.update_tag()
# this indicates that process of the tree is finished and next scene event can be skipped
# the scene trigger will try to update all trees, so they all should be marked
for t in BlTrees().sv_main_trees:
t['SKIP_UPDATE'] = True
gc.enable()
sv_logger.debug(f'Global update - {int((time() - self._start_time) * 1000)}ms')
del self._start_time
@cached_property
def _start_time(self):
"""Start time of execution the whole queue of tasks"""
return time()
@cached_property
def _main_area(self) -> Optional:
"""Searching appropriate area index for reporting update progress"""
if not self.current:
return
for area in bpy.context.screen.areas:
if area.ui_type == 'SverchCustomTreeType':
path = area.spaces[0].path
# it appeared the tree already can be invalid when undo event
# is called pretty fast
try:
if path and path[-1].node_tree.name == self._current.tree.name:
return area
except ReferenceError:
# probably all reports should be cleaned through search
sv_logger.debug(f"Unable report a nodes updating progress")
def _report_progress(self, text: str = None):
"""Show text in the tree editor header. If text is none the header
returns in its initial condition"""
if self._main_area:
self._main_area.header_text_set(text)
def __repr__(self):
return f"<Tasks current={self._current} todo={self._todo}>"
tasks = Tasks()
def tree_event_loop(delay):
"""Sverchok tasks handler"""
with catch_log_error():
if tasks:
tasks.run()
return delay
tree_event_loop = partial(tree_event_loop, 0.01)
class Task:
"""Generator which should update some node tree. The task is hashable, and
it is equal to another task if booth of them update the same tree.
The generator is suspendable and can limit its execution by given time"""
def __init__(self, tree, updater, is_scene_update):
""":tree: tree which should be updated
:_updater: generator which should update given tree
:is_exhausted: the status of the generator - read only
:last_node: last node which going to be processed by the generator
- read only"""
self.tree: SvTree = tree
self.is_scene_update: bool = is_scene_update
self.is_exhausted = False
self.last_node = None
self._updater: Generator = updater
self.__hash__ = cache(self.__hash__)
def run(self, max_duration):
"""Starts the tree updating
:max_duration: if updating of the tree takes more time than given
maximum duration it saves its state and returns execution flow"""
duration = 0
try:
start_time = time()
while duration < max_duration:
self.last_node = next(self._updater)
duration = time() - start_time
return duration
except StopIteration:
self.is_exhausted = True
return duration
def throw(self, error: CancelError):
"""Should be used to cansel tree execution. Updater should add
the error to current node and abort the execution"""
self._updater.throw(error)
self.is_exhausted = True
def __eq__(self, other: 'Task'):
return self.tree.tree_id == other.tree.tree_id
def __hash__(self):
return hash(self.tree.tree_id)
def __repr__(self):
return f"<Task: {self.tree.name}>"
@post_load_call
def post_load_register():
# when new file is loaded all timers are unregistered
# to make them persistent the post load handler should be used
# but it's also is possible that the timer was registered during registration of the add-on
if not bpy.app.timers.is_registered(tree_event_loop):
bpy.app.timers.register(tree_event_loop)
def register():
"""Registration of Sverchok event handler"""
# it appeared that the timers can be registered during the add-on initialization
# The timer should be registered here because post_load_register won't be called when an add-on is enabled by user
bpy.app.timers.register(tree_event_loop)
def unregister():
bpy.app.timers.unregister(tree_event_loop)
Functions
def post_load_register()
-
Expand source code
@post_load_call def post_load_register(): # when new file is loaded all timers are unregistered # to make them persistent the post load handler should be used # but it's also is possible that the timer was registered during registration of the add-on if not bpy.app.timers.is_registered(tree_event_loop): bpy.app.timers.register(tree_event_loop)
def register()
-
Registration of Sverchok event handler
Expand source code
def register(): """Registration of Sverchok event handler""" # it appeared that the timers can be registered during the add-on initialization # The timer should be registered here because post_load_register won't be called when an add-on is enabled by user bpy.app.timers.register(tree_event_loop)
def unregister()
-
Expand source code
def unregister(): bpy.app.timers.unregister(tree_event_loop)
Classes
class Task (tree, updater, is_scene_update)
-
Generator which should update some node tree. The task is hashable, and it is equal to another task if booth of them update the same tree. The generator is suspendable and can limit its execution by given time
:tree: tree which should be updated :_updater: generator which should update given tree :is_exhausted: the status of the generator - read only :last_node: last node which going to be processed by the generator - read only
Expand source code
class Task: """Generator which should update some node tree. The task is hashable, and it is equal to another task if booth of them update the same tree. The generator is suspendable and can limit its execution by given time""" def __init__(self, tree, updater, is_scene_update): """:tree: tree which should be updated :_updater: generator which should update given tree :is_exhausted: the status of the generator - read only :last_node: last node which going to be processed by the generator - read only""" self.tree: SvTree = tree self.is_scene_update: bool = is_scene_update self.is_exhausted = False self.last_node = None self._updater: Generator = updater self.__hash__ = cache(self.__hash__) def run(self, max_duration): """Starts the tree updating :max_duration: if updating of the tree takes more time than given maximum duration it saves its state and returns execution flow""" duration = 0 try: start_time = time() while duration < max_duration: self.last_node = next(self._updater) duration = time() - start_time return duration except StopIteration: self.is_exhausted = True return duration def throw(self, error: CancelError): """Should be used to cansel tree execution. Updater should add the error to current node and abort the execution""" self._updater.throw(error) self.is_exhausted = True def __eq__(self, other: 'Task'): return self.tree.tree_id == other.tree.tree_id def __hash__(self): return hash(self.tree.tree_id) def __repr__(self): return f"<Task: {self.tree.name}>"
Methods
def run(self, max_duration)
-
Starts the tree updating :max_duration: if updating of the tree takes more time than given maximum duration it saves its state and returns execution flow
Expand source code
def run(self, max_duration): """Starts the tree updating :max_duration: if updating of the tree takes more time than given maximum duration it saves its state and returns execution flow""" duration = 0 try: start_time = time() while duration < max_duration: self.last_node = next(self._updater) duration = time() - start_time return duration except StopIteration: self.is_exhausted = True return duration
def throw(self, error: CancelError)
-
Should be used to cansel tree execution. Updater should add the error to current node and abort the execution
Expand source code
def throw(self, error: CancelError): """Should be used to cansel tree execution. Updater should add the error to current node and abort the execution""" self._updater.throw(error) self.is_exhausted = True
class Tasks
-
It keeps tasks which should be executed and executes the on demand. 1. Execute tasks 2. Time the whole execution 3. Display the progress in the UI
:_todo: list of tasks to run :_current: task which was started to execute
Expand source code
class Tasks: """ It keeps tasks which should be executed and executes the on demand. 1. Execute tasks 2. Time the whole execution 3. Display the progress in the UI """ _todo: set['Task'] _current: Optional['Task'] def __init__(self): """:_todo: list of tasks to run :_current: task which was started to execute""" self._todo = set() self._current = None def __bool__(self): """Has anything to do?""" return bool(self._current or self._todo) def add(self, task: 'Task'): """Add new tasks to run them via timer""" if task.is_scene_update: # scene event can be excepted only as first event # in all other cases it can be generated by tree evaluation # and can lead to infinite loop of updates if self._current is not None: return # print(f"Add {task=}") self._todo.add(task) @profile(section="UPDATE") def run(self): """Run given tasks to update trees and report execution process in the header of a node tree editor""" # 0.15 is max timer frequency max_duration = 10 if self.current.is_scene_update else 0.15 duration = 0 while self.current: if duration > max_duration: return # print(f"Run task: {self.current}") duration += self.current.run(max_duration-duration) if self.current.last_node: msg = f'Pres "ESC" to abort, updating node "{self.current.last_node.name}"' self._report_progress(msg) if self.current.is_exhausted: self._next() self._finish() def cancel(self): """Remove all tasks in the queue and abort current one""" self._todo.clear() if self._current: try: self._current.throw(CancelError) except (StopIteration, RuntimeError): pass finally: # protection from the task to be stack forever self._finish() @property def current(self) -> Optional['Task']: """Return current task if it is absent it tries to pop it from the tasks queue if it's empty returns None""" if self._current: return self._current elif self._todo: self._start() self._current = self._todo.pop() return self._current else: return None def _start(self): """Preprocessing before executing the whole queue of events""" self._start_time gc.disable() # for performance def _next(self): """Should be called to switch to next tasks when current is exhausted It made some cleanups after the previous task""" self._report_progress() self._current = self._todo.pop() if self._todo else None del self._main_area def _finish(self): """Cleanups. Also triggers scene handler and mark trees to skip it""" self._report_progress() del self._main_area # this only need to trigger scene changes handler again # todo should be proved that this is right location to call from bpy.context.scene.update_tag() # this indicates that process of the tree is finished and next scene event can be skipped # the scene trigger will try to update all trees, so they all should be marked for t in BlTrees().sv_main_trees: t['SKIP_UPDATE'] = True gc.enable() sv_logger.debug(f'Global update - {int((time() - self._start_time) * 1000)}ms') del self._start_time @cached_property def _start_time(self): """Start time of execution the whole queue of tasks""" return time() @cached_property def _main_area(self) -> Optional: """Searching appropriate area index for reporting update progress""" if not self.current: return for area in bpy.context.screen.areas: if area.ui_type == 'SverchCustomTreeType': path = area.spaces[0].path # it appeared the tree already can be invalid when undo event # is called pretty fast try: if path and path[-1].node_tree.name == self._current.tree.name: return area except ReferenceError: # probably all reports should be cleaned through search sv_logger.debug(f"Unable report a nodes updating progress") def _report_progress(self, text: str = None): """Show text in the tree editor header. If text is none the header returns in its initial condition""" if self._main_area: self._main_area.header_text_set(text) def __repr__(self): return f"<Tasks current={self._current} todo={self._todo}>"
Instance variables
var current : Optional[Task]
-
Return current task if it is absent it tries to pop it from the tasks queue if it's empty returns None
Expand source code
@property def current(self) -> Optional['Task']: """Return current task if it is absent it tries to pop it from the tasks queue if it's empty returns None""" if self._current: return self._current elif self._todo: self._start() self._current = self._todo.pop() return self._current else: return None
Methods
def add(self, task: Task)
-
Add new tasks to run them via timer
Expand source code
def add(self, task: 'Task'): """Add new tasks to run them via timer""" if task.is_scene_update: # scene event can be excepted only as first event # in all other cases it can be generated by tree evaluation # and can lead to infinite loop of updates if self._current is not None: return # print(f"Add {task=}") self._todo.add(task)
def cancel(self)
-
Remove all tasks in the queue and abort current one
Expand source code
def cancel(self): """Remove all tasks in the queue and abort current one""" self._todo.clear() if self._current: try: self._current.throw(CancelError) except (StopIteration, RuntimeError): pass finally: # protection from the task to be stack forever self._finish()
def run(*args, **kwargs)
-
Run given tasks to update trees and report execution process in the header of a node tree editor
Expand source code
def wrapper(*args, **kwargs): if is_profiling_enabled(section): global _profile_nesting profile = get_global_profile() _profile_nesting += 1 if _profile_nesting == 1: profile.enable() result = func(*args, **kwargs) _profile_nesting -= 1 if _profile_nesting == 0: profile.disable() return result else: return func(*args, **kwargs)