Коммит 20704e6c создал по автору Durman's avatar Durman
Просмотр файлов

add code documentation

владелец 41351866
......@@ -6,12 +6,8 @@
# License-Filename: LICENSE
"""
Purpose of this module is centralization of update events.
For now it can be used in debug mode for understanding which event method are triggered by Blender
during evaluation of Python code.
Details: https://github.com/nortikin/sverchok/issues/3077
Events keep information about which Blender trigger was executed and with which
context
"""
from __future__ import annotations
......@@ -29,8 +25,8 @@ if TYPE_CHECKING:
class TreeEvent:
"""Keeps information about what was changed during the even"""
# task should be run via timer only https://developer.blender.org/T82318#1053877
"""Adding removing nodes or links but not necessarily"""
# the event should be run via timer only https://developer.blender.org/T82318#1053877
tree: SvTree
def __init__(self, tree):
......@@ -41,10 +37,12 @@ class TreeEvent:
class ForceEvent(TreeEvent):
"""Indicates the whole tree should be recalculated"""
pass
class AnimationEvent(TreeEvent):
"""Frame was changed. Last event can be with the same frame"""
is_frame_changed: bool
is_animation_playing: bool
......@@ -55,10 +53,12 @@ class AnimationEvent(TreeEvent):
class SceneEvent(TreeEvent):
"""Something was changed in the scene"""
pass
class PropertyEvent(TreeEvent):
"""Property of the node(s) was changed"""
updated_nodes: Iterable[SvNode]
def __init__(self, tree, updated_nodes):
......@@ -67,6 +67,7 @@ class PropertyEvent(TreeEvent):
class GroupTreeEvent(TreeEvent):
"""The same as Tree event but inside a group tree"""
tree: GrTree
update_path: list[GrNode]
......@@ -76,6 +77,7 @@ class GroupTreeEvent(TreeEvent):
class GroupPropertyEvent(GroupTreeEvent):
"""Property of a node(s) inside a group tree was changed"""
updated_nodes: Iterable[SvNode]
def __init__(self, tree, update_path, update_nodes):
......@@ -84,8 +86,11 @@ class GroupPropertyEvent(GroupTreeEvent):
class FileEvent:
"""It indicates that new file was loaded"""
pass
class TreesGraphEvent:
"""It indicates that something was changed in trees relations defined via
group nodes"""
pass
......@@ -20,11 +20,10 @@ def control_center(event):
1. Update tree model lazily
2. Check whether the event should be processed
3. Process event or create task to process via timer"""
was_executed = False
was_executed = True
# property of some node of a group tree was changed
if type(event) is ev.GroupPropertyEvent:
was_executed = True
gr_tree = GroupUpdateTree.get(event.tree)
gr_tree.add_outdated(event.updated_nodes)
gr_tree.update_path = event.update_path
......@@ -35,10 +34,9 @@ def control_center(event):
# topology of a group tree was changed
elif type(event) is ev.GroupTreeEvent:
was_executed = True
gr_tree = GroupUpdateTree.get(event.tree)
gr_tree.is_updated = False
# gr_tree.update_path = event.update_path
gr_tree.update_path = event.update_path
for main_tree in trees_graph[event.tree]:
us.UpdateTree.get(main_tree).add_outdated(trees_graph[main_tree, event.tree])
if main_tree.sv_process:
......@@ -46,16 +44,25 @@ def control_center(event):
# Connections between trees were changed
elif type(event) is ev.TreesGraphEvent:
was_executed = True
trees_graph.is_updated = False
else:
was_executed = False
return was_executed
class GroupUpdateTree(us.UpdateTree):
get: Callable[['GrTree'], 'GroupUpdateTree']
"""Group trees has their own update method separate from main tree to have
more nice profiling statistics. Also, it keeps some specific to grop trees
statuses."""
get: Callable[['GrTree'], 'GroupUpdateTree'] # type hinting does not work grate :/
def update(self, node: 'GrNode'):
"""Updates outdated nodes of group tree. Also, it keeps proper state of
the exec_path. If exec_path is equal to update path it also updates UI
of the tree
:node: group node which tree is executed"""
self._exec_path.append(node)
try:
is_opened_tree = self.update_path == self._exec_path
......@@ -71,7 +78,7 @@ class GroupUpdateTree(us.UpdateTree):
if is_opened_tree:
if self._tree.show_time_mode == "Cumulative":
times = self.calc_cam_update_time()
times = self._calc_cam_update_time()
else:
times = None
us.update_ui(self._tree, times)
......@@ -82,6 +89,13 @@ class GroupUpdateTree(us.UpdateTree):
self._exec_path.pop()
def __init__(self, tree):
"""Should node be used directly but wia the get class method
:update_path: list of group nodes via which update trigger was executed
:_exec_path: list of group nodes via which the tree is executed
:_viewer_nodes: output nodes which should be updated. If not presented
all output nodes will be updated. The main reason of having them is to
update viewer nodes only in opened group tree, as a side effect it
optimises nodes execution"""
super().__init__(tree)
# update UI for the tree opened under the given path
self.update_path: list['GrNode'] = []
......@@ -94,6 +108,11 @@ class GroupUpdateTree(us.UpdateTree):
self._copy_attrs.extend(['_exec_path', 'update_path', '_viewer_nodes'])
def _walk(self) -> tuple[Node, list[NodeSocket]]:
"""Yields nodes in order of their proper execution. It starts yielding
from outdated nodes. It keeps the outdated_nodes storage in proper
state. It checks after yielding the error status of the node. If the
node has error it goes into outdated_nodes. If tree has viewer nodes
it yields only nodes which should be called to update viewers."""
# walk all nodes in the tree
if self._outdated_nodes is None:
outdated = None
......@@ -104,7 +123,7 @@ class GroupUpdateTree(us.UpdateTree):
else:
outdated = frozenset(self._outdated_nodes)
viewers = frozenset(self._viewer_nodes)
self._outdated_nodes.clear() # todo what if execution was canceled?
self._outdated_nodes.clear()
self._viewer_nodes.clear()
for node, other_socks in self._sort_nodes(outdated, viewers):
......@@ -118,10 +137,17 @@ class GroupUpdateTree(us.UpdateTree):
class TreesGraph:
"""It keeps relationships between main trees and group trees."""
_group_main: dict['GrTree', set['SvTree']]
_entry_nodes: dict['SvTree', dict['GrTree', set['SvNode']]]
def __init__(self):
""":is_updated: the graph can be marked as outdated in this case it will
be updated automatically whenever data will be fetched from it
:_group_main: it stores information about in which main trees a group
tree is used. The group tree can be located in some nested groups too
:_entry_nodes: it stores information about which group nodes in main
tree should be called to update a group tree"""
self.is_updated = False
self._group_main = defaultdict(set)
......@@ -133,7 +159,8 @@ class TreesGraph:
def __getitem__(self, item: tuple['SvTree', 'GrTree']) -> set['SvNode']: ...
def __getitem__(self, item):
# print(self)
"""It either returns related to given group tree Main tree or collection
of group nodes to update given group tree"""
if not self.is_updated:
self._update()
if isinstance(item, tuple):
......@@ -143,7 +170,7 @@ class TreesGraph:
return self._group_main[item]
def _update(self):
# print("REFRESH TreesGraph")
"""Calculate relationships between group trees and main trees"""
self._group_main.clear()
self._entry_nodes.clear()
for tree in BlTrees().sv_main_trees:
......@@ -154,6 +181,7 @@ class TreesGraph:
@staticmethod
def _walk(from_: NodeTree) -> Iterator[tuple[NodeTree, 'SvNode']]:
"""Iterate over all nested node trees"""
current_entry_node = None
def next_(_tree):
......
......@@ -16,6 +16,7 @@ if TYPE_CHECKING:
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
......@@ -24,17 +25,23 @@ class Tasks:
_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"""
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"""
max_duration = 0.15 # 0.15 is max timer frequency
duration = 0
......@@ -52,6 +59,7 @@ class Tasks:
self._finish()
def cancel(self):
"""Remove all tasks in the queue and abort current one"""
self._todo.clear()
if self._current:
try:
......@@ -63,6 +71,8 @@ class Tasks:
@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:
......@@ -73,15 +83,19 @@ class Tasks:
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
......@@ -100,6 +114,7 @@ class Tasks:
@cached_property
def _start_time(self):
"""Start time of execution the whole queue of tasks"""
return time()
@cached_property
......@@ -114,6 +129,8 @@ class Tasks:
return area
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)
......@@ -133,7 +150,15 @@ 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):
""":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_exhausted = False
self.last_node = None
......@@ -142,6 +167,9 @@ class Task:
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()
......@@ -154,14 +182,12 @@ class Task:
self.is_exhausted = True
return duration
def throw(self, error):
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
@property
def id(self):
return self.tree.tree_id
def __eq__(self, other: 'Task'):
return self.tree.tree_id == other.tree.tree_id
......
......@@ -30,28 +30,25 @@ def control_center(event):
1. Update tree model lazily
2. Check whether the event should be processed
3. Process event or create task to process via timer"""
was_executed = False
was_executed = True
# frame update
# This event can't be handled via NodesUpdater during animation rendering
# because new frame change event can arrive before timer finishes its tusk.
# Or timer can start working before frame change is handled.
if type(event) is ev.AnimationEvent:
was_executed = True
if event.tree.sv_animate:
UpdateTree.get(event.tree).is_animation_updated = False
UpdateTree.update_animation(event)
# something changed in the scene
elif type(event) is ev.SceneEvent:
was_executed = True
if event.tree.sv_scene_update and event.tree.sv_process:
UpdateTree.get(event.tree).is_scene_updated = False
ts.tasks.add(ts.Task(event.tree, UpdateTree.main_update(event.tree)))
# nodes changed properties
elif type(event) is ev.PropertyEvent:
was_executed = True
tree = UpdateTree.get(event.tree)
tree.add_outdated(event.updated_nodes)
if event.tree.sv_process:
......@@ -59,26 +56,29 @@ def control_center(event):
# update the whole tree anyway
elif type(event) is ev.ForceEvent:
was_executed = True
UpdateTree.reset_tree(event.tree)
ts.tasks.add(ts.Task(event.tree, UpdateTree.main_update(event.tree)))
# mark that the tree topology has changed
elif type(event) is ev.TreeEvent:
was_executed = True
UpdateTree.get(event.tree).is_updated = False
if event.tree.sv_process:
ts.tasks.add(ts.Task(event.tree, UpdateTree.main_update(event.tree)))
# new file opened
elif type(event) is ev.FileEvent:
was_executed = True
UpdateTree.reset_tree()
else:
was_executed = False
return was_executed
class SearchTree:
"""Data structure which represents Blender node trees but with ability
of efficient search tree elements. Also it keeps tree state so it can be
compared with new one to define differences."""
_from_nodes: dict['SvNode', set['SvNode']]
_to_nodes: dict['SvNode', set['SvNode']]
_from_sock: dict[NodeSocket, NodeSocket]
......@@ -107,6 +107,7 @@ class SearchTree:
self._remove_wifi_nodes()
def nodes_from(self, from_nodes: Iterable['SvNode']) -> set['SvNode']:
"""Returns all next nodes from given ones"""
def node_walker_to(node_: 'SvNode'):
for nn in self._to_nodes.get(node_, []):
yield nn
......@@ -114,6 +115,7 @@ class SearchTree:
return set(bfs_walk(from_nodes, node_walker_to))
def nodes_to(self, to_nodes: Iterable['SvNode']) -> set['SvNode']:
"""Returns all previous nodes from given ones"""
def node_walker_from(node_: 'SvNode'):
for nn in self._from_nodes.get(node_, []):
yield nn
......@@ -121,6 +123,7 @@ class SearchTree:
return set(bfs_walk(to_nodes, node_walker_from))
def sort_nodes(self, nodes: Iterable['SvNode']) -> list['SvNode']:
"""Returns nodes in order of their correct execution"""
walk_structure: dict[SvNode, set[SvNode]] = defaultdict(set)
for n in nodes:
if n in self._from_nodes:
......@@ -131,11 +134,17 @@ class SearchTree:
nodes.append(node)
return nodes
def previous_sockets(self, node: 'SvNode') -> list[NodeSocket]:
def previous_sockets(self, node: 'SvNode') -> list[Optional[NodeSocket]]:
"""Return output sockets connected to input ones of given node
If input socket is not linked the output socket will be None"""
return [self._from_sock.get(s) for s in node.inputs]
def update_node(self, node: 'SvNode', supress=True):
with AddStatistic(node, supress):
def update_node(self, node: 'SvNode', suppress=True):
"""Fetches data from previous node, makes data conversion if connected
sockets have different types, calls process method of the given node
records nodes statistics
If suppress is True an error during node execution will be suppressed"""
with AddStatistic(node, suppress):
prepare_input_data(self.previous_sockets(node), node.inputs)
node.process()
......@@ -251,15 +260,16 @@ class SearchTree:
class UpdateTree(SearchTree):
"""It catches some data for more efficient searches compare to Blender
tree data structure"""
"""It caches the trees to keep outdated nodes and to perform tree updating
efficiently."""
_tree_catch: dict[str, 'UpdateTree'] = dict() # the module should be auto-reloaded to prevent crashes
@classmethod
def get(cls, tree: "SvTree", refresh_tree=False) -> "UpdateTree":
"""
Get cached tree. If tree was not cached it will be.
:refresh_tree: if True it will convert update flags into outdated
nodes. This can be expensive so it should be called only before tree
nodes. This can be expensive, so it should be called only before tree
reevaluation
"""
if tree.tree_id not in cls._tree_catch:
......@@ -291,6 +301,7 @@ class UpdateTree(SearchTree):
@classmethod
@profile(section="UPDATE")
def update_animation(cls, event: ev.AnimationEvent):
"""Should be called to updated animated nodes"""
try:
g = cls.main_update(event.tree, event.is_frame_changed, not event.is_animation_playing)
while True:
......@@ -300,8 +311,11 @@ class UpdateTree(SearchTree):
@classmethod
def main_update(cls, tree: NodeTree, update_nodes=True, update_interface=True) -> Generator['SvNode', None, None]:
"""Only for main trees
1. Whe it called the tree should have information of what is outdated"""
"""Thi generator is for the triggers. It can update outdated nodes and
update UI. Should be used only with main trees, the group trees should
use different method to separate profiling statistics. Whe it called the
tree should have information of what is outdated"""
# print(f"UPDATE NODES {event.type=}, {event.tree.name=}")
up_tree = cls.get(tree, refresh_tree=True)
if update_nodes:
......@@ -318,14 +332,14 @@ class UpdateTree(SearchTree):
if update_interface:
if up_tree._tree.show_time_mode == "Cumulative":
times = up_tree.calc_cam_update_time()
times = up_tree._calc_cam_update_time()
else:
times = None
update_ui(tree, times)
@classmethod
def reset_tree(cls, tree: NodeTree = None):
"""Remove tree data or data of all trees"""
"""Remove tree data or data of all trees from the cache"""
if tree is not None and tree.tree_id in cls._tree_catch:
del cls._tree_catch[tree.tree_id]
else:
......@@ -333,29 +347,29 @@ class UpdateTree(SearchTree):
def copy(self) -> 'UpdateTree':
"""They copy will be with new topology if original tree was changed
since berth of the first tree. Other attributes copied as is."""
since instancing of the first tree. Other attributes copied as is."""
copy_ = type(self)(self._tree)
for attr in self._copy_attrs:
setattr(copy_, attr, copy(getattr(self, attr)))
return copy_
def add_outdated(self, nodes: Iterable):
"""Add outdated nodes explicitly. Animation and scene dependent nodes
can be marked as outdated via dedicated flags for performance."""
if self._outdated_nodes is not None:
self._outdated_nodes.update(nodes)
def calc_cam_update_time(self) -> Iterable['SvNode']:
cum_time_nodes = dict() # don't have frame nodes
for node, prev_socks in self.__sort_nodes():
prev_nodes = self._from_nodes[node]
if len(prev_nodes) > 1:
cum_time = sum(n.get(TIME_KEY, 0) for n in self.nodes_to([node]))
else:
cum_time = sum(cum_time_nodes.get(n, 0) for n in prev_nodes)
cum_time += node.get(TIME_KEY, 0)
cum_time_nodes[node] = cum_time
return (cum_time_nodes.get(n) for n in self._tree.nodes)
def __init__(self, tree: NodeTree):
"""Should not use be used directly, only via the get class method
:is_updated: Should be False if topology of the tree was changed
:is_animation_updated: Should be False animation dependent nodes should
be updated
:is_scene_updated: Should be False if scene dependent nodes should be
updated
:_outdated_nodes: Keeps nodes which properties were changed or which
have errors. Can be None when what means that all nodes are outdated
:_copy_attrs: list of attributes which should be copied by the copy
method"""
super().__init__(tree)
self._tree_catch[tree.tree_id] = self
......@@ -375,6 +389,7 @@ class UpdateTree(SearchTree):
]
def _animation_nodes(self) -> set['SvNode']:
"""Returns nodes which are animation dependent"""
an_nodes = set()
if not self.is_animation_updated:
for node in self._tree.nodes:
......@@ -384,6 +399,7 @@ class UpdateTree(SearchTree):
return an_nodes
def _scene_nodes(self) -> set['SvNode']:
"""Returns nodes which are scene dependent"""
sc_nodes = set()
if not self.is_scene_updated:
for node in self._tree.nodes:
......@@ -393,6 +409,13 @@ class UpdateTree(SearchTree):
return sc_nodes
def _walk(self) -> tuple[Node, list[NodeSocket]]:
"""Yields nodes in order of their proper execution. It starts yielding
from outdated nodes. It keeps the outdated_nodes storage in proper
state. It checks after yielding the error status of the node. If the
node has error it goes into outdated_nodes. It uses cached walker, so
it works more efficient when outdated nodes are the same between the
method calls."""
# walk all nodes in the tree
if self._outdated_nodes is None:
outdated = None
......@@ -415,6 +438,12 @@ class UpdateTree(SearchTree):
from_nodes: frozenset['SvNode'] = None,
to_nodes: frozenset['SvNode'] = None)\
-> list[tuple['SvNode', list[NodeSocket]]]:
"""Sort nodes of the tree in proper execution order. Whe all given
parameters are None it uses all tree nodes
:from_nodes: if given it sorts only next nodes from given ones
:to_nodes: if given it sorts only previous nodes from given
If from_nodes and to_nodes are given it uses only intersection of next
nodes from from_nodes and previous nodes from to_nodes"""
nodes_to_walk = set()
walk_structure = None
if from_nodes is None and to_nodes is None:
......@@ -442,6 +471,9 @@ class UpdateTree(SearchTree):
return nodes
def _update_difference(self, old: 'UpdateTree') -> set['SvNode']:
"""Returns nodes which should be updated according to changes in the
tree topology
:old: previous state of the tree to compare with"""
nodes_to_update = self._from_nodes.keys() - old._from_nodes.keys()
new_links = self._links - old._links
for from_sock, to_sock in new_links:
......@@ -455,7 +487,22 @@ class UpdateTree(SearchTree):
nodes_to_update.add(old._sock_node[to_sock])
return nodes_to_update
def _calc_cam_update_time(self) -> Iterable['SvNode']:
"""Return cumulative update time in order of node_group.nodes collection"""
cum_time_nodes = dict() # don't have frame nodes
for node, prev_socks in self.__sort_nodes():
prev_nodes = self._from_nodes[node]
if len(prev_nodes) > 1:
cum_time = sum(n.get(TIME_KEY, 0) for n in self.nodes_to([node]))
else:
cum_time = sum(cum_time_nodes.get(n, 0) for n in prev_nodes)
cum_time += node.get(TIME_KEY, 0)
cum_time_nodes[node] = cum_time
return (cum_time_nodes.get(n) for n in self._tree.nodes)
def _debug_color(self, walker: Generator, use_color: bool = True):
"""Colorize nodes which were previously executed. Before execution, it
resets all dbug colors"""
def _set_color(node: 'SvNode', _use_color: bool):
use_key = "DEBUG_use_user_color"
color_key = "DEBUG_user_color"
......@@ -485,9 +532,14 @@ class UpdateTree(SearchTree):
class AddStatistic:
"""It caches errors during execution of process method of a node and saves
update time, update status and error"""
# this probably can be inside the Node class as an update method
# using context manager from contextlib has big overhead
# https://stackoverflow.com/questions/26152934/why-the-staggering-overhead-50x-of-contextlib-and-the-with-statement-in-python
def __init__(self, node: 'SvNode', supress=True):
""":supress: if True any errors during node execution will be suppressed"""
self._node = node
self._start = perf_counter()
self._supress = supress
......@@ -511,7 +563,11 @@ class AddStatistic:
return issubclass(exc_type, Exception)
def prepare_input_data(prev_socks, input_socks):
def prepare_input_data(prev_socks: list[Optional[NodeSocket]],
input_socks: list[NodeSocket]):
"""Reads data from given outputs socket make it conversion if necessary and
put data into input given socket"""
# this can be a socket method
for ps, ns in zip(prev_socks, input_socks):
if ps is None:
continue
......@@ -526,6 +582,9 @@ def prepare_input_data(prev_socks, input_socks):
def update_ui(tree: NodeTree, times: Iterable[float] = None):
"""Updates UI of the given tree
:times: optional node timing in order of group_tree.nodes collection"""
# probably this can be moved to tree.update_ui method
errors = (n.get(ERROR_KEY, None) for n in tree.nodes)
times = times or (n.get(TIME_KEY, 0) for n in tree.nodes)
tree.update_ui(errors, times)
......@@ -365,7 +365,7 @@ class DNA:
tree.sv_process = True
for node in exec_order:
try:
s_tree.update_node(node, supress=False)
s_tree.update_node(node, suppress=False)
except Exception:
raise
......
......@@ -251,7 +251,7 @@ class SvLoopOutNode(SverchCustomTreeNode, bpy.types.Node):
print(f"Looping Object Number {idx}")
for node in sort_loop_nodes[1:-1]:
try:
tree.update_node(node, supress=False)
tree.update_node(node, suppress=False)
except Exception:
raise Exception(f"Element: {idx}")
......@@ -308,7 +308,7 @@ class SvLoopOutNode(SverchCustomTreeNode, bpy.types.Node):
print(f"Looping iteration Number {i+1}")
for node in sort_loop_nodes[1:-1]:
try:
tree.update_node(node, supress=False)
tree.update_node(node, suppress=False)
except Exception:
raise Exception(f"Iteration number: {i+1}")
......
Поддерживает Markdown
0% или .
You are about to add 0 people to the discussion. Proceed with caution.
Сначала завершите редактирование этого сообщения!
Пожалуйста, зарегистрируйтесь или чтобы прокомментировать