From 0b6c85c89bca7328244901b1e76bcb93e5396722 Mon Sep 17 00:00:00 2001 From: "Quantizr (Jimmy)" <9859727+Quantizr@users.noreply.github.com> Date: Sat, 6 Sep 2025 00:34:51 -0700 Subject: [PATCH] don't delete item handlers, add locks, don't expand large lists --- tools/jotpluggler/views.py | 137 +++++++++++++++++++------------------ 1 file changed, 69 insertions(+), 68 deletions(-) diff --git a/tools/jotpluggler/views.py b/tools/jotpluggler/views.py index 7c143fc9af..f3b75a2de1 100644 --- a/tools/jotpluggler/views.py +++ b/tools/jotpluggler/views.py @@ -198,9 +198,10 @@ class TimeSeriesPanel(ViewPanel): class DataTreeNode: - def __init__(self, name: str, full_path: str = ""): + def __init__(self, name: str, full_path: str = "", parent = None): self.name = name self.full_path = full_path + self.parent = parent self.children: dict[str, DataTreeNode] = {} self.is_leaf = False self.child_count = 0 @@ -225,6 +226,7 @@ class DataTreeView: self.data_manager.add_observer(self._on_data_loaded) self.queued_search = None self.new_data = False + self._ui_lock = threading.RLock() def create_ui(self, parent_tag: str): with dpg.child_window(parent=parent_tag, border=False, width=-1, height=-1): @@ -264,7 +266,7 @@ class DataTreeView: if i < len(parts) - 1: parent_nodes_to_recheck.add(current_node) # for incremental changes from new data if part not in current_node.children: - current_node.children[part] = DataTreeNode(name=part, full_path=current_path_prefix) + current_node.children[part] = DataTreeNode(name=part, full_path=current_path_prefix, parent=current_node) current_node = current_node.children[part] if not current_node.is_leaf: @@ -278,45 +280,42 @@ class DataTreeView: return target_tree def update_frame(self, font): - if self.avg_char_width is None and dpg.is_dearpygui_running(): - self.avg_char_width = self.calculate_avg_char_width(font) - - if self.new_data: - current_paths = set(self.data_manager.get_all_paths()) - new_paths = current_paths - self._all_paths_cache - if new_paths: - all_paths_empty = not self._all_paths_cache - self._all_paths_cache = current_paths - if all_paths_empty: - self._populate_tree() - else: - self._add_paths_to_tree(new_paths, incremental=True) - self.new_data = False - - if self.queued_search is not None: - self.current_search = self.queued_search - self._all_paths_cache = set(self.data_manager.get_all_paths()) - self._populate_tree() - self.queued_search = None - - nodes_processed = 0 - while self.build_queue and nodes_processed < self.MAX_NODES_PER_FRAME: - child_node, parent_tag, before_tag = self.build_queue.popleft() - if not child_node.ui_created: - if child_node.is_leaf: - self._create_leaf_ui(child_node, parent_tag, before_tag) - else: - self._create_tree_node_ui(child_node, parent_tag, before_tag) - nodes_processed += 1 + with self._ui_lock: + if self.avg_char_width is None and dpg.is_dearpygui_running(): + self.avg_char_width = self.calculate_avg_char_width(font) + + if self.new_data: + current_paths = set(self.data_manager.get_all_paths()) + new_paths = current_paths - self._all_paths_cache + if new_paths: + all_paths_empty = not self._all_paths_cache + self._all_paths_cache = current_paths + if all_paths_empty: + self._populate_tree() + else: + self._add_paths_to_tree(new_paths, incremental=True) + self.new_data = False + + if self.queued_search is not None: + self.current_search = self.queued_search + self._all_paths_cache = set(self.data_manager.get_all_paths()) + self._populate_tree() + self.queued_search = None + + nodes_processed = 0 + while self.build_queue and nodes_processed < self.MAX_NODES_PER_FRAME: + child_node, parent_tag, before_tag = self.build_queue.popleft() + if not child_node.ui_created: + if child_node.is_leaf: + self._create_leaf_ui(child_node, parent_tag, before_tag) + else: + self._create_tree_node_ui(child_node, parent_tag, before_tag) + nodes_processed += 1 def search_data(self): self.queued_search = dpg.get_value("search_input") def _clear_ui(self): - for handler_tag in self._item_handlers: - dpg.delete_item(handler_tag) - self._item_handlers.clear() - if dpg.does_item_exist("data_tree_container"): dpg.delete_item("data_tree_container", children_only=True) @@ -341,6 +340,9 @@ class DataTreeView: label = f"{node.name} ({node.child_count} fields)" search_term = self.current_search.strip().lower() should_open = bool(search_term) and len(search_term) > 1 and any(search_term in path for path in self._get_descendant_paths(node)) + if should_open and node.parent and node.parent.child_count > 100 and node.child_count > 2: + label += " (+)" + should_open = False with dpg.tree_node(label=label, parent=parent_tag, tag=tag, default_open=should_open, open_on_arrow=True, open_on_double_click=True, before=before): with dpg.item_handler_registry(tag=handler_tag): @@ -374,43 +376,42 @@ class DataTreeView: node.ui_tag = f"value_{node.full_path}" def _on_item_visible(self, sender, app_data, user_data): - path = user_data - group_tag = f"group_{path}" - value_tag = f"value_{path}" + with self._ui_lock: + path = user_data + group_tag = f"group_{path}" + value_tag = f"value_{path}" - if not self.avg_char_width or not dpg.does_item_exist(group_tag) or not dpg.does_item_exist(value_tag): - return + if not self.avg_char_width or not dpg.does_item_exist(group_tag) or not dpg.does_item_exist(value_tag): + return - value_column_width = dpg.get_item_rect_size("sidebar_window")[0] // 2 - dpg.configure_item(group_tag, xoffset=value_column_width) + value_column_width = dpg.get_item_rect_size("sidebar_window")[0] // 2 + dpg.configure_item(group_tag, xoffset=value_column_width) - value = self.data_manager.get_value_at(path, self.playback_manager.current_time_s) - if value is not None: - formatted_value = self.format_and_truncate(value, value_column_width, self.avg_char_width) - dpg.set_value(value_tag, formatted_value) - else: - dpg.set_value(value_tag, "N/A") + value = self.data_manager.get_value_at(path, self.playback_manager.current_time_s) + if value is not None: + formatted_value = self.format_and_truncate(value, value_column_width, self.avg_char_width) + dpg.set_value(value_tag, formatted_value) + else: + dpg.set_value(value_tag, "N/A") def _request_children_build(self, node: DataTreeNode, handler_tag=None): - if not node.children_ui_created and (node.name == "root" or (node.ui_tag is not None and dpg.get_value(node.ui_tag))): # check root or node expanded - if handler_tag and dpg.does_item_exist(handler_tag): - dpg.delete_item(handler_tag) - self._item_handlers.discard(handler_tag) - parent_tag = "data_tree_container" if node.name == "root" else node.ui_tag - sorted_children = sorted(node.children.values(), key=self._natural_sort_key) - - for i, child_node in enumerate(sorted_children): - if not child_node.ui_created: - before_tag: int | str = 0 - for j in range(i + 1, len(sorted_children)): # when incrementally building get "before_tag" for correct ordering - next_child = sorted_children[j] - if next_child.ui_created: - candidate_tag = f"group_{next_child.full_path}" if next_child.is_leaf else f"tree_{next_child.full_path}" - if dpg.does_item_exist(candidate_tag): - before_tag = candidate_tag - break - self.build_queue.append((child_node, parent_tag, before_tag)) - node.children_ui_created = True + with self._ui_lock: + if not node.children_ui_created and (node.name == "root" or (node.ui_tag is not None and dpg.get_value(node.ui_tag))): # check root or node expanded + parent_tag = "data_tree_container" if node.name == "root" else node.ui_tag + sorted_children = sorted(node.children.values(), key=self._natural_sort_key) + + for i, child_node in enumerate(sorted_children): + if not child_node.ui_created: + before_tag: int | str = 0 + for j in range(i + 1, len(sorted_children)): # when incrementally building get "before_tag" for correct ordering + next_child = sorted_children[j] + if next_child.ui_created: + candidate_tag = f"group_{next_child.full_path}" if next_child.is_leaf else f"tree_{next_child.full_path}" + if dpg.does_item_exist(candidate_tag): + before_tag = candidate_tag + break + self.build_queue.append((child_node, parent_tag, before_tag)) + node.children_ui_created = True def _should_show_path(self, path: str, search_term: str) -> bool: if 'DEPRECATED' in path and not os.environ.get('SHOW_DEPRECATED'):