Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[viewer] provide more task info in task list #102

Merged
merged 5 commits into from
Sep 7, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 72 additions & 51 deletions src/lifeblood_viewer/graphics_items.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
from datetime import timedelta
from .code_editor.editor import StringParameterEditor
from .node_extra_items import ImplicitSplitVisualizer
from .network_item import NetworkItemWithUI, NetworkItem
from .network_item_watchers import NetworkItemWatcher, WatchableNetworkItem

from lifeblood.config import get_config
from lifeblood.uidata import NodeUi, Parameter, ParameterExpressionError, ParametersLayoutBase, OneLineParametersLayout, CollapsableVerticalGroup, Separator, MultiGroupLayout
Expand All @@ -25,7 +27,7 @@

import imgui

from typing import TYPE_CHECKING, Optional, List, Tuple, Dict, Set, Callable, Iterable, Union
from typing import FrozenSet, TYPE_CHECKING, Optional, List, Tuple, Dict, Set, Callable, Iterable, Union

from . import nodeeditor
from .editor_scene_integration import fetch_and_open_log_viewer
Expand All @@ -46,28 +48,6 @@ def length2(v: QPointF):
return QPointF.dotProduct(v, v)


class NetworkItem(QGraphicsItem):
def __init__(self, id):
super().__init__()
self.__id = id

def get_id(self):
return self.__id


class NetworkItemWithUI(NetworkItem):
def update_ui(self):
self.update() # currently contents and UI are drawn always together, so this will do
# but in future TODO: invalidate only UI layer

def draw_imgui_elements(self, drawing_widget):
"""
this should only be called from active opengl context!
:return:
"""
pass


class TaskAnimation(QAbstractAnimation):
def __init__(self, task: "Task", node2: "Node", pos2: "QPointF", duration: int, parent):
super(TaskAnimation, self).__init__(parent)
Expand Down Expand Up @@ -106,7 +86,7 @@ def updateCurrentTime(self, currentTime: int) -> None:
self.__task.setPos(pos)


class Node(NetworkItemWithUI):
class Node(NetworkItemWithUI, NetworkItemWatcher, WatchableNetworkItem):
class TaskSortOrder(Enum):
ID = 0

Expand Down Expand Up @@ -301,7 +281,7 @@ def set_expanded(self, expanded: bool):
self.__vismark.setPos(QPointF(0, self._get_nodeshape().boundingRect().height() * 0.5))

for i, task in enumerate(self.__tasks):
task.set_node_animated(self, *self.get_task_pos(task, i))
task._set_node_animated(self, *self.get_task_pos(task, i))

def input_snap_points(self):
# TODO: cache snap points, don't recalc them every time
Expand Down Expand Up @@ -369,6 +349,18 @@ def output_nodes(self, outname: Optional[str] = None) -> Set["Node"]:
nodes.update(con.input()[0] for con in self.output_connections(con_name))
return nodes

def add_item_watcher(self, watcher: "NetworkItemWatcher"):
super().add_item_watcher(watcher)
if len(self.item_watchers()) == 1: # first watcher
for task in self.__tasks:
task.add_item_watcher(self)

def remove_item_watcher(self, watcher: "NetworkItemWatcher"):
super().remove_item_watcher(watcher)
if len(self.item_watchers()) == 0: # removed last watcher
for task in self.__tasks:
task.remove_item_watcher(self)

def boundingRect(self) -> QRectF:
if self.__cached_bounds is None:
lw = self.__width + self.__line_width
Expand Down Expand Up @@ -523,9 +515,9 @@ def add_task(self, task: "Task", animated=True):
self.update() # cuz node displays task number - we should redraw
pos_id = len(self.__tasks)
if task.node() is None or not animated:
task.set_node(self, *self.get_task_pos(task, pos_id))
task._set_node(self, *self.get_task_pos(task, pos_id))
else:
task.set_node_animated(self, *self.get_task_pos(task, pos_id))
task._set_node_animated(self, *self.get_task_pos(task, pos_id))

insert_at = self._find_insert_index_for_task(task, prefer_back=True)

Expand All @@ -535,11 +527,14 @@ def add_task(self, task: "Task", animated=True):
self.__tasks.append(None) # temporary placeholder, it'll be eliminated either in the loop, or after if task is last
for i in reversed(range(insert_at + 1, len(self.__tasks))):
self.__tasks[i] = self.__tasks[i-1] # TODO: animated param should affect below!
self.__tasks[i].set_node_animated(self, *self.get_task_pos(self.__tasks[i], i))
self.__tasks[i]._set_node_animated(self, *self.get_task_pos(self.__tasks[i], i))
self.__tasks[insert_at] = task
self.__tasks[insert_at].set_node_animated(self, *self.get_task_pos(task, insert_at))
self.__tasks[insert_at]._set_node_animated(self, *self.get_task_pos(task, insert_at))
task._Task__node = self

if len(self.item_watchers()) > 0:
task.add_item_watcher(self)

def remove_tasks(self, tasks_to_remove: Iterable["Task"]):
"""
this should cause much less animation overhead compared to
Expand All @@ -549,6 +544,8 @@ def remove_tasks(self, tasks_to_remove: Iterable["Task"]):
tasks_to_remove = set(tasks_to_remove)
for task in tasks_to_remove:
task._Task__node = None
if len(self.item_watchers()) > 0:
task.remove_item_watcher(self)
#task.set_node(None) # no, currently causes bad recursion

# invalidate sorted cache
Expand All @@ -564,7 +561,7 @@ def remove_tasks(self, tasks_to_remove: Iterable["Task"]):
off += 1
else:
self.__tasks[i - off] = self.__tasks[i]
self.__tasks[i - off].set_node_animated(self, *self.get_task_pos(self.__tasks[i - off], i - off))
self.__tasks[i - off]._set_node_animated(self, *self.get_task_pos(self.__tasks[i - off], i - off))
self.__tasks = self.__tasks[:-off]
for x in tasks_to_remove:
assert x not in self.__tasks
Expand All @@ -581,7 +578,7 @@ def remove_task(self, task_to_remove: "Task"):
task_to_remove._Task__node = None
for i in range(task_pid, len(self.__tasks) - 1):
self.__tasks[i] = self.__tasks[i + 1]
self.__tasks[i].set_node_animated(self, *self.get_task_pos(self.__tasks[i], i))
self.__tasks[i]._set_node_animated(self, *self.get_task_pos(self.__tasks[i], i))
self.__tasks = self.__tasks[:-1]
assert task_to_remove not in self.__tasks
self.update() # cuz node displays task number - we should redraw
Expand Down Expand Up @@ -654,9 +651,9 @@ def task_state_changed(self, task):
# place where it has to be
for i in reversed(range(append_at + 1, idx+1)):
self.__tasks[i] = self.__tasks[i-1]
self.__tasks[i].set_node_animated(self, *self.get_task_pos(self.__tasks[i], i))
self.__tasks[i]._set_node_animated(self, *self.get_task_pos(self.__tasks[i], i))
self.__tasks[append_at] = task
self.__tasks[append_at].set_node_animated(self, *self.get_task_pos(task, append_at))
self.__tasks[append_at]._set_node_animated(self, *self.get_task_pos(task, append_at))

#
# interface
Expand Down Expand Up @@ -1317,7 +1314,7 @@ def itemChange(self, change: QGraphicsItem.GraphicsItemChange, value):
return super(NodeConnection, self).itemChange(change, value)


class Task(NetworkItemWithUI):
class Task(NetworkItemWithUI, WatchableNetworkItem):
__brushes = None
__borderpen = None
__paused_pen = None
Expand All @@ -1341,6 +1338,7 @@ def __init__(self, task_data: TaskData):
# self.__groups = set() if groups is None else set(groups)
self.__log: Dict[int, Dict[int, Union[IncompleteInvocationLogData, InvocationLogData]]] = {}
self.__inv_log: Optional[List[Tuple[int, int, Union[IncompleteInvocationLogData, InvocationLogData]]]] = None # for presentation - inv_id -> (node_id, log)
self.__inv_stat_total_time: Optional[Tuple[float, float]] = None
self.__ui_attributes: dict = {}
self.__ui_env_res_attributes: Optional[EnvironmentResolverArguments] = None
self.__requested_invocs_while_selected = set()
Expand Down Expand Up @@ -1604,6 +1602,16 @@ def set_progress(self, progress: float):
def get_progress(self) -> Optional[float]:
return self.__raw_data.progress if self.__raw_data else None

def add_item_watcher(self, watcher: "NetworkItemWatcher"):
super().add_item_watcher(watcher)
# additionally refresh ui if we are not being watched
if len(self.item_watchers()) == 1: # it's a first watcher
self.refresh_ui()

def __reset_cached_invocation_data(self):
self.__inv_log = None
self.__inv_stat_total_time = None

def update_log(self, alllog: Dict[int, Dict[int, Union[IncompleteInvocationLogData, InvocationLogData]]], full_update: bool):
"""
This function gets called by scene with new shit from worker. Maybe there's more sense to make it "_protected"
Expand Down Expand Up @@ -1636,7 +1644,7 @@ def update_log(self, alllog: Dict[int, Dict[int, Union[IncompleteInvocationLogDa
invocs.pop(inv_id)

# clear cached inverted dict, it will be rebuilt on next access
self.__inv_log = None
self.__reset_cached_invocation_data()

self.update_ui()

Expand All @@ -1648,10 +1656,29 @@ def remove_invocations_log(self, invocation_ids: List[int]):
invocs.pop(invocation_id)

# clear cached inverted dict, it will be rebuilt on next access
self.__inv_log = None
self.__reset_cached_invocation_data()

self.update_ui()

def invocations_total_time(self, only_last_per_node: bool = True) -> float:
if self.__inv_stat_total_time is None:
total_time = 0.0
max_inv_logs = {}
for inv_id, node_id, log in self.invocation_logs():
if log.invocation_runtime is None:
continue
if node_id not in max_inv_logs or max_inv_logs[node_id][0] < inv_id: # looking for invocation with biggest inv_id, as it will be the latest
max_inv_logs[node_id] = (inv_id, log)
total_time += log.invocation_runtime

total_latest_time = sum(x.invocation_runtime for _, x in max_inv_logs.values())
self.__inv_stat_total_time = (total_latest_time, total_time)

if only_last_per_node:
return self.__inv_stat_total_time[0]
else:
return self.__inv_stat_total_time[1]

def invocation_logs(self) -> List[Tuple[int, int, Union[IncompleteInvocationLogData, InvocationLogData]]]:
"""
TODO: ensure immutable!
Expand All @@ -1675,8 +1702,9 @@ def set_environment_attributes(self, env_attrs: Optional[EnvironmentResolverArgu
def environment_attributes(self) -> Optional[EnvironmentResolverArguments]:
return self.__ui_env_res_attributes

def set_node(self, node: Optional[Node], pos: Optional[QPointF] = None, layer: Optional[int] = None):
def _set_node(self, node: Optional[Node], pos: Optional[QPointF] = None, layer: Optional[int] = None):
"""
helper to be called by Node
"""
need_ui_update = node != self.__node

Expand All @@ -1695,10 +1723,13 @@ def set_node(self, node: Optional[Node], pos: Optional[QPointF] = None, layer: O
if need_ui_update:
self.refresh_ui()

def set_node_animated(self, node: Optional[Node], pos: QPointF, layer: int):
def _set_node_animated(self, node: Optional[Node], pos: QPointF, layer: int):
"""
helper to be called by Node
"""
# first try to optimize, if we move on the same node to invisible layer - dont animate
if node == self.__node and layer >= self.__visible_layers_count and self.__animation_group is None:
return self.set_node(node, pos, layer)
return self._set_node(node, pos, layer)
#
dist = ((pos if node is None else node.mapToScene(pos)) - self.final_scene_position())
ldist = sqrt(QPointF.dotProduct(dist, dist))
Expand Down Expand Up @@ -1765,10 +1796,10 @@ def setParentItem(self, item):
def refresh_ui(self):
"""
unlike update - this method actually queries new task ui status
if task is not selected - does nothing
if task is not selected or not watched- does nothing
:return:
"""
if not self.isSelected():
if not self.isSelected() and len(self.item_watchers()) == 0:
return
self.scene().request_log_meta(self.get_id()) # update all task metadata: which nodes it ran on and invocation numbers only
self.scene().request_attributes(self.get_id())
Expand All @@ -1781,16 +1812,6 @@ def refresh_ui(self):
or invoc_dict.invocation_state != InvocationState.FINISHED) and invoc_id in self.__requested_invocs_while_selected:
self.__requested_invocs_while_selected.remove(invoc_id)

# # if task is in progress - we find that invocation of it that is not finished and null it to force update
# if self.__state == TaskState.IN_PROGRESS \
# and self.__node.get_id() in self.__log \
# and self.__log[self.__node.get_id()] is not None:
# for invoc_id, invoc in self.__log[self.__node.get_id()].items():
# if (invoc is None or
# invoc['state'] != InvocationState.FINISHED.value) \
# and invoc_id in self.__requested_invocs_while_selected:
# self.__requested_invocs_while_selected.remove(invoc_id)

def itemChange(self, change, value):
if change == QGraphicsItem.ItemSelectedHasChanged:
if value and self.__node is not None: # item was just selected
Expand Down
30 changes: 30 additions & 0 deletions src/lifeblood_viewer/network_item.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
from PySide2.QtWidgets import QGraphicsItem


class NetworkItem(QGraphicsItem):
def __init__(self, id):
super().__init__()

# cheat cuz Shiboken.Object does not respect mro
mro = self.__class__.mro()
cur_mro_i = mro.index(NetworkItem)
if len(mro) > cur_mro_i + 2:
super(mro[cur_mro_i+2], self).__init__()

self.__id = id

def get_id(self):
return self.__id


class NetworkItemWithUI(NetworkItem):
def update_ui(self):
self.update() # currently contents and UI are drawn always together, so this will do
# but in future TODO: invalidate only UI layer

def draw_imgui_elements(self, drawing_widget):
"""
this should only be called from active opengl context!
:return:
"""
pass
30 changes: 30 additions & 0 deletions src/lifeblood_viewer/network_item_watchers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
from .network_item import NetworkItem

from typing import FrozenSet, Set


class NetworkItemWatcher:
def task_was_updated(self, item: NetworkItem):
pass


class WatchableNetworkItem:
def __init__(self):
super().__init__()
self.__task_watchers: Set["NetworkItemWatcher"] = set()

def item_watchers(self) -> FrozenSet["NetworkItemWatcher"]:
return frozenset(self.__task_watchers)

def add_item_watcher(self, watcher: "NetworkItemWatcher"):
"""
watcher observes the task, therefore all task's metadata must be updated,
watchers need to be properly notified on update too.
"""
self.__task_watchers.add(watcher)

def remove_item_watcher(self, watcher: "NetworkItemWatcher"):
self.__task_watchers.remove(watcher)

def has_item_watcher(self, watcher: "NetworkItemWatcher") -> bool:
return watcher in self.__task_watchers
Loading
Loading