De-duplicate dependency tree calculations and data structures, de-clutter notebook

This commit is contained in:
Maximilian Schmeller 2022-10-13 19:13:39 +09:00
parent 130c99e56f
commit b1dc01b101
9 changed files with 711 additions and 978 deletions

View file

@ -0,0 +1,201 @@
import math
import re
import graphviz as gv
import latency_graph.latency_graph_structure as lg
from matching.subscriptions import sanitize
from tracing_interop.tr_types import TrContext
NODE_COLORS = {
"sensing": {"fill": "#e1d5e7", "stroke": "#9673a6"},
"localization": {"fill": "#dae8fc", "stroke": "#6c8ebf"},
"perception": {"fill": "#d5e8d4", "stroke": "#82b366"},
"planning": {"fill": "#fff2cc", "stroke": "#d6b656"},
"control": {"fill": "#ffe6cc", "stroke": "#d79b00"},
"system": {"fill": "#f8cecc", "stroke": "#b85450"},
"vehicle_interface": {"fill": "#b0e3e6", "stroke": "#0e8088"},
None: {"fill": "#f5f5f5", "stroke": "#666666"}
}
NODE_NAMESPACE_MAPPING = {
'perception': 'perception',
'sensing': 'sensing',
'planning': 'planning',
'control': 'control',
'awapi': 'system',
'autoware_api': 'system',
'map': 'system',
'system': 'system',
'localization': 'localization',
'robot_state_publisher': None,
'aggregator_node': None,
'pointcloud_container': 'sensing',
}
def plot_latency_graph_full(lat_graph: lg.LatencyGraph, tr: TrContext, filename: str):
# Compare with: https://autowarefoundation.github.io/autoware-documentation/main/design/autoware-architecture/node-diagram/
g = gv.Digraph('G', filename="latency_graph.gv",
node_attr={'shape': 'plain'},
graph_attr={'pack': '1'})
g.graph_attr['rankdir'] = 'LR'
def plot_hierarchy(gv_parent, lg_node: lg.LGHierarchyLevel, **subgraph_kwargs):
if lg_node.name == "[NONE]":
return
print(f"{' ' * lg_node.full_name.count('/')}Processing {lg_node.name}: {len(lg_node.callbacks)}")
with gv_parent.subgraph(name=f"cluster_{lg_node.full_name.replace('/', '__')}", **subgraph_kwargs) as c:
c.attr(label=lg_node.name)
for cb in lg_node.callbacks:
if isinstance(cb, lg.LGTrCallback):
tr_cb = cb.cb
try:
sym = tr.callback_symbols.by_id.get(tr_cb.callback_object)
pretty_sym = repr(sanitize(sym.symbol))
except KeyError:
pretty_sym = cb.name
except TypeError:
pretty_sym = cb.name
else:
pretty_sym = cb.name
pretty_sym = pretty_sym.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;")
c.node(cb.id(),
f'<<table BORDER="0" CELLBORDER="1" CELLSPACING="0"><tr><td port="in"></td><td>{pretty_sym}</td><td port="out"></td></tr></table>>')
for ch in lg_node.children:
plot_hierarchy(c, ch, **subgraph_kwargs)
def plot_lg(graph: lg.LatencyGraph):
for top_level_node in graph.top_node.children:
colors = NODE_COLORS[NODE_NAMESPACE_MAPPING.get(top_level_node.name)]
plot_hierarchy(g, top_level_node, graph_attr={'bgcolor': colors["fill"], 'pencolor': colors["stroke"]})
for edge in graph.edges:
g.edge(f"{edge.start.id()}:out", f"{edge.end.id()}:in")
plot_lg(lat_graph)
g.save(f"{filename}.gv")
g.render(f"{filename}.svg")
return g
def plot_latency_graph_overview(lat_graph: lg.LatencyGraph, excl_node_patterns, input_node_patterns,
output_node_patterns, max_hier_level, filename):
##################################################
# Compute in/out topics for hierarchy level X
##################################################
def get_nodes_on_level(lat_graph: lg.LatencyGraph):
def _traverse_node(node: lg.LGHierarchyLevel, cur_lvl=0):
if cur_lvl == max_hier_level:
return [node]
if not node.children:
return [node]
collected_nodes = []
for ch in node.children:
collected_nodes += _traverse_node(ch, cur_lvl + 1)
return collected_nodes
return _traverse_node(lat_graph.top_node)
lvl_nodes = get_nodes_on_level(lat_graph)
lvl_nodes = [n for n in lvl_nodes if not any(re.search(p, n.full_name) for p in excl_node_patterns)]
input_nodes = [n.full_name for n in lvl_nodes if any(re.search(p, n.full_name) for p in input_node_patterns)]
output_nodes = [n.full_name for n in lvl_nodes if any(re.search(p, n.full_name) for p in output_node_patterns)]
print(', '.join(map(lambda n: n, input_nodes)))
print(', '.join(map(lambda n: n, output_nodes)))
print(', '.join(map(lambda n: n.full_name, lvl_nodes)))
def _collect_callbacks(n: lg.LGHierarchyLevel):
callbacks = []
callbacks += n.callbacks
for ch in n.children:
callbacks += _collect_callbacks(ch)
return callbacks
cb_to_node_map = {}
for n in lvl_nodes:
cbs = _collect_callbacks(n)
for cb in cbs:
cb_to_node_map[cb.id()] = n
edges_between_nodes = {}
for edge in lat_graph.edges:
from_node = cb_to_node_map.get(edge.start.id())
to_node = cb_to_node_map.get(edge.end.id())
if from_node is None or to_node is None:
continue
if from_node.full_name == to_node.full_name:
continue
k = (from_node.full_name, to_node.full_name)
if k not in edges_between_nodes:
edges_between_nodes[k] = []
edges_between_nodes[k].append(edge)
g = gv.Digraph('G', filename="latency_graph.gv",
node_attr={'shape': 'plain'},
graph_attr={'pack': '1'})
g.graph_attr['rankdir'] = 'LR'
for n in lvl_nodes:
colors = NODE_COLORS[NODE_NAMESPACE_MAPPING.get(n.full_name.strip("/").split("/")[0])]
peripheries = "1" if n.full_name not in output_nodes else "2"
g.node(n.full_name, label=n.full_name, fillcolor=colors["fill"], color=colors["stroke"],
shape="box", style="filled", peripheries=peripheries)
if n.full_name in input_nodes:
helper_node_name = f"{n.full_name}__before"
g.node(helper_node_name, label="", shape="none", height="0", width="0")
g.edge(helper_node_name, n.full_name)
def compute_e2e_paths(start_nodes, end_nodes, edges):
frontier_paths = [[n] for n in start_nodes]
final_paths = []
while frontier_paths:
frontier_paths_new = []
for path in frontier_paths:
head = path[-1]
if head in end_nodes:
final_paths.append(path)
continue
out_nodes = [n_to for n_from, n_to in edges if n_from == head if n_to not in path]
new_paths = [path + [n] for n in out_nodes]
frontier_paths_new += new_paths
frontier_paths = frontier_paths_new
final_paths = [[(n_from, n_to)
for n_from, n_to in zip(path[:-1], path[1:])]
for path in final_paths]
return final_paths
e2e_paths = compute_e2e_paths(input_nodes, output_nodes, edges_between_nodes)
for (src_name, dst_name), edges in edges_between_nodes.items():
print(src_name, dst_name, len(edges))
color = "black" if any((src_name, dst_name) in path for path in e2e_paths) else "tomato"
g.edge(src_name, dst_name, penwidth=str(math.log(len(edges)) * 2 + .2), color=color)
g.save(f"{filename}.gv")
g.render(f"{filename}.svg")
return g

View file

@ -1,18 +1,16 @@
from bisect import bisect_left, bisect_right from bisect import bisect_left, bisect_right
from collections import defaultdict from collections import defaultdict
from dataclasses import dataclass from dataclasses import dataclass
from itertools import combinations
from multiprocessing import Pool
from typing import Optional, Set, List, Iterable, Dict, Tuple
from functools import cache from functools import cache
from itertools import combinations
from typing import Optional, Set, List, Iterable, Dict
from uuid import UUID, uuid4
import numpy as np
from tqdm import tqdm from tqdm import tqdm
from tqdm.contrib import concurrent
from matching.subscriptions import sanitize from matching.subscriptions import sanitize
from tracing_interop.tr_types import TrContext, TrCallbackObject, TrCallbackSymbol, TrNode, TrPublisher, TrSubscription, \ from tracing_interop.tr_types import TrContext, TrCallbackObject, TrCallbackSymbol, TrNode, TrPublisher, TrTimer, \
TrTimer, TrPublishInstance, TrSubscriptionObject, TrTopic, TrCallbackInstance, Timestamp TrSubscriptionObject, TrTopic
TOPIC_FILTERS = ["/parameter_events", "/tf_static", "/robot_description", "diagnostics", "/rosout"] TOPIC_FILTERS = ["/parameter_events", "/tf_static", "/robot_description", "diagnostics", "/rosout"]
@ -203,6 +201,8 @@ class LatencyGraph:
cb_pubs: Dict[TrCallbackObject, Set[TrPublisher]] cb_pubs: Dict[TrCallbackObject, Set[TrPublisher]]
pub_cbs: Dict[TrPublisher, Set[TrCallbackObject]] pub_cbs: Dict[TrPublisher, Set[TrCallbackObject]]
_uuid: UUID
def __init__(self, tr: TrContext): def __init__(self, tr: TrContext):
################################################## ##################################################
# Annotate nodes with their callbacks # Annotate nodes with their callbacks
@ -290,3 +290,10 @@ class LatencyGraph:
################################################## ##################################################
self.top_node = _hierarchize(lg_nodes) self.top_node = _hierarchize(lg_nodes)
self._uuid = uuid4()
def __hash__(self):
return hash(self._uuid)
def __eq__(self, other):
return isinstance(other, LatencyGraph) and other._uuid == self._uuid

View file

@ -1,31 +0,0 @@
from dataclasses import dataclass
from typing import List
from tracing_interop.tr_types import TrPublishInstance, TrCallbackInstance
@dataclass
class DepTree:
head: TrCallbackInstance | TrPublishInstance
deps: List['DepTree']
def depth(self):
return 1 + max(map(DepTree.depth, self.deps), default=0)
def size(self):
return 1 + sum(map(DepTree.size, self.deps))
def fanout(self):
if not self.deps:
return 1
return sum(map(DepTree.fanout, self.deps))
def e2e_lat(self):
return self.head.timestamp - self.critical_path()[-1].timestamp
def critical_path(self):
if not self.deps:
return [self.head]
return [self.head, *min(map(DepTree.critical_path, self.deps), key=lambda ls: ls[-1].timestamp)]

0
message_tree/__init__.py Normal file
View file

View file

@ -0,0 +1,335 @@
import re
from bisect import bisect_right
from collections import defaultdict
from functools import cache
from typing import List
from tqdm import tqdm
from latency_graph.latency_graph_structure import LatencyGraph
from matching.subscriptions import sanitize
from message_tree.message_tree_structure import DepTree, E2EBreakdownItem
from tracing_interop.tr_types import TrCallbackInstance, TrPublishInstance, TrPublisher, TrCallbackObject, TrContext, \
TrSubscriptionObject, TrTimer, TrNode, TrTopic
__dep_tree_cache = {}
def _repr(inst: TrCallbackInstance | TrPublishInstance):
"""
If a string representation is found for `inst`, it is returned, else an empty string is returned.
"""
match inst:
case TrPublishInstance(publisher=pub):
return pub.topic_name if pub else ""
case TrCallbackInstance(callback_obj=cb_obj):
cb_obj: TrCallbackObject
return repr(sanitize(cb_obj.callback_symbol.symbol)) if cb_obj and cb_obj.callback_symbol else ""
raise TypeError(f"Argument has to be callback or publish instance, is {type(inst).__name__}")
def get_dep_tree(inst: TrPublishInstance | TrCallbackInstance, lat_graph: LatencyGraph, tr: TrContext,
excluded_path_patterns, time_limit_s):
"""
Finds all (desired) dependencies of a publish/callback instance and returns a dependency tree.
The dependencies can be filtered via `time_limit_s`, which is the maximum difference in start time between `inst`
and any dependency in the tree.
Another filter is `excluded_path_patterns`, which cuts off paths where one instance's string representation matches
any of the given regex patterns.
"""
start_time = inst.timestamp
def __get_dep_tree(inst, is_dep_cb, visited=None):
# If inst owner has been visited already, skip (loop prevention)
if visited is not None and owner(inst) in visited:
return None
# If we want to retrieve the tree, look in the cache first
cache_key = (inst, is_dep_cb)
if cache_key in __dep_tree_cache:
return __dep_tree_cache[cache_key]
if visited is None:
visited = tuple()
if any(re.search(p, _repr(inst)) for p in excluded_path_patterns):
return None
if inst.timestamp - start_time > time_limit_s:
return None
children_are_dep_cbs = False
match inst:
case TrPublishInstance(publisher=pub):
deps = [get_msg_dep_cb(inst, lat_graph)]
case TrCallbackInstance() as cb_inst:
cb_inst: TrCallbackInstance
deps = [inst_get_dep_msg(cb_inst, tr)]
if not is_dep_cb:
deps += inst_get_dep_insts(cb_inst, tr)
children_are_dep_cbs = True
case _:
print("[WARN] Expected inst to be of type TrPublishInstance or TrCallbackInstance, "
f"got {type(inst).__name__}")
return None
# print("Rec level", lvl)
deps = list(filter(None, deps))
# Copy visited set for each child because we don't want separate paths to interfere with each other
deps = [__get_dep_tree(dep, children_are_dep_cbs, {*visited, owner(inst)}) for dep in deps]
deps = list(filter(None, deps))
# Create tree instance, cache and return it
ret_tree = DepTree(inst, deps)
__dep_tree_cache[cache_key] = ret_tree
return ret_tree
return __get_dep_tree(inst, False)
def build_dep_trees(end_topics, lat_graph, tr, excluded_path_patterns, time_limit_s):
"""
Builds the dependency trees for all messages published in any of `end_topics` and returns them as a list.
"""
all_trees = []
for end_topic in end_topics:
end_topic: TrTopic
print(f"====={end_topic.name}")
pubs = end_topic.publishers
for pub in pubs:
msgs = list(pub.instances)
for msg in tqdm(msgs, desc="Processing output messages"):
msg: TrPublishInstance
tree = get_dep_tree(msg, lat_graph, tr, excluded_path_patterns, time_limit_s)
all_trees.append(tree)
return all_trees
def inst_get_dep_msg(inst: TrCallbackInstance, tr: TrContext):
if inst.callback_object not in tr.callback_objects.by_callback_object:
# print("Callback not found (2)")
return None
if not isinstance(inst.callback_obj.owner, TrSubscriptionObject):
# print(f"Wrong type: {type(inst.callback_obj.owner)}")
return None
sub_obj: TrSubscriptionObject = inst.callback_obj.owner
if sub_obj and sub_obj.subscription and sub_obj.subscription.topic:
# print(f"Subscription has no topic")
pubs = sub_obj.subscription.topic.publishers
else:
pubs = []
def _pub_latest_msg_before(pub: TrPublisher, inst):
i_latest_msg = bisect_right(pub.instances, inst.timestamp, key=lambda x: x.timestamp) - 1
if i_latest_msg < 0 or i_latest_msg >= len(pub.instances):
return None
latest_msg = pub.instances[i_latest_msg]
if latest_msg.timestamp >= inst.timestamp:
return None
return latest_msg
msgs = [_pub_latest_msg_before(pub, inst) for pub in pubs]
msgs = list(filter(None, msgs))
msgs.sort(key=lambda i: i.timestamp, reverse=True)
if msgs:
msg = msgs[0]
return msg
# print(f"No messages found for topic {sub_obj.subscription.topic}")
return None
def inst_get_dep_insts(inst: TrCallbackInstance, tr: TrContext):
if inst.callback_object not in tr.callback_objects.by_callback_object:
# print("Callback not found")
return []
dep_cbs = get_cb_dep_cbs(inst.callback_obj, tr)
def _cb_to_chronological_inst(cb: TrCallbackObject, inst):
i_inst_latest = bisect_right(cb.callback_instances, inst.timestamp, key=lambda x: x.timestamp)
for inst_before in cb.callback_instances[i_inst_latest::-1]:
if inst_before.t_end < inst.timestamp:
return inst_before
return None
insts = [_cb_to_chronological_inst(cb, inst) for cb in dep_cbs]
insts = list(filter(None, insts))
return insts
def get_cb_dep_cbs(cb: TrCallbackObject, tr: TrContext):
match cb.owner:
case TrSubscriptionObject() as sub_obj:
sub_obj: TrSubscriptionObject
owner = sub_obj.subscription.node
case TrTimer() as tmr:
tmr: TrTimer
owner = tmr.node
case _:
raise RuntimeError(f"Encountered {cb.owner} as callback owner")
owner: TrNode
dep_sub_objs = {sub.subscription_object for sub in owner.subscriptions}
dep_cbs = {tr.callback_objects.by_id.get(sub_obj.id) for sub_obj in dep_sub_objs if sub_obj is not None}
dep_cbs |= {tr.callback_objects.by_id.get(tmr.id) for tmr in owner.timers}
dep_cbs.discard(cb)
dep_cbs.discard(None)
return dep_cbs
def get_msg_dep_cb(msg: TrPublishInstance, lat_graph: LatencyGraph):
"""
For a given message instance `msg`, find the publishing callback,
as well as the message instances that callback depends on (transitively within its TrNode).
"""
# Find CB instance that published msg
# print(f"PUB {msg.publisher.node.path if msg.publisher.node is not None else '??'} ==> {msg.publisher.topic_name}")
pub_cbs = lat_graph.pub_cbs.get(msg.publisher)
if pub_cbs is None:
# print("Publisher unknown to lat graph. Skipping.")
return None
# print(f"Found {len(pub_cbs)} pub cbs")
cb_inst_candidates = []
for cb in pub_cbs:
# print(f" > CB ({len(cb.callback_instances)} instances): {cb.callback_symbol.symbol if cb.callback_symbol else cb.id}")
i_inst_after = bisect_right(cb.callback_instances, msg.timestamp, key=lambda x: x.timestamp)
for inst in cb.callback_instances[:i_inst_after]:
if msg.timestamp > inst.t_end:
continue
assert inst.t_start <= msg.timestamp <= inst.t_end
cb_inst_candidates.append(inst)
if len(cb_inst_candidates) > 1:
# print("Found multiple possible callbacks")
return None
if not cb_inst_candidates:
# print("Found no possible callbacks")
return None
dep_inst = cb_inst_candidates[0]
return dep_inst
def e2e_paths_sorted_desc(tree: DepTree, input_topic_patterns):
"""
Return all paths through `tree` that start with a callback instance publishing on a topic matching any of
`input_topic_patterns`. The paths are sorted by length in a descending manner (element 0 is longest).
"""
def _collect_all_paths(t: DepTree):
return [(*_collect_all_paths(d), t.head) for d in t.deps]
def _trim_path(path):
valid_input = False
i = -1
for i, inst in enumerate(path):
match inst:
case TrPublishInstance(publisher=pub):
pub: TrPublisher
if pub and any(re.search(p, pub.topic_name) for p in input_topic_patterns):
valid_input = True
break
if not valid_input:
return None
if i == 0 or not isinstance(path[i - 1], TrCallbackInstance):
print(f"[WARN] Message has no publishing callback in dep tree.")
return path[i:] # Return path from first message that fits an input topic pattern
return path[i - 1:] # Return path from its publishing callback if it exists
paths = _collect_all_paths(tree)
paths = list(filter(lambda p: p is not None, map(_trim_path, tqdm(paths, desc="_trim_path"))))
paths.sort(key=lambda path: path[-1].timestamp - path[0].timestamp, reverse=True)
return paths
def e2e_latency_breakdown(path: list):
"""
Separates E2E latency into a sequence of dds, idle, and cpu times.
This method expects a publish instance at the last position in `path`.
The return format is a list of the form [("<type>", <time>), ("<type>", <time>), ...] with type bein gone of the
three mentioned above.
"""
ret_list: List[E2EBreakdownItem] = []
cb_inst: TrCallbackInstance
cb_inst_prev: TrCallbackInstance
pub_inst: TrPublishInstance
pub_inst_prev: TrPublishInstance
last_inst = None
for inst in path:
match inst:
case TrCallbackInstance() as cb_inst:
match last_inst:
case TrCallbackInstance() as cb_inst_prev:
ret_list.append(E2EBreakdownItem("idle", cb_inst.t_start - cb_inst_prev.t_end,
(cb_inst_prev, cb_inst)))
case TrPublishInstance() as pub_inst_prev:
ret_list.append(E2EBreakdownItem("dds", cb_inst.t_start - pub_inst_prev.timestamp,
(pub_inst_prev, inst)))
case TrPublishInstance() as pub_inst:
match last_inst:
case TrCallbackInstance() as cb_inst_prev:
ret_list.append(E2EBreakdownItem("cpu", pub_inst.timestamp - cb_inst_prev.t_start,
(cb_inst_prev, pub_inst)))
case TrPublishInstance():
raise TypeError(f"Found two publish instances in a row in an E2E path.")
last_inst = inst
if not isinstance(last_inst, TrPublishInstance):
raise TypeError(f"Last instance in path is not a message but a {type(last_inst).__name__}")
return ret_list
@cache
def owner(inst: TrCallbackInstance | TrPublishInstance):
match inst:
case TrCallbackInstance(callback_obj=cb_obj):
cb_obj: TrCallbackObject
if cb_obj and cb_obj.callback_symbol:
sym = repr(sanitize(cb_obj.callback_symbol.symbol))
else:
sym = str(cb_obj.id)
return sym
case TrPublishInstance(publisher=pub):
pub: TrPublisher
topic = pub.topic_name
return topic
case _:
raise ValueError()
def _repr_path(path: List[TrPublishInstance | TrCallbackInstance]):
return " -> ".join(map(owner, path))
def aggregate_e2e_paths(paths: List[List[TrPublishInstance | TrCallbackInstance]]):
path_cohorts = defaultdict(list)
for path in paths:
key = _repr_path(path)
path_cohorts[key].append(path)
return path_cohorts

View file

@ -0,0 +1,49 @@
from typing import List
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.axes import Axes
from matplotlib.figure import Figure
from message_tree.message_tree_structure import E2EBreakdownItem
def e2e_breakdown_type_hist(items: List[E2EBreakdownItem]):
"""
Given a list of e2e breakdown instances of the form `("<type>", <duration>)`, plots a histogram for each encountered
type.
"""
plot_types = ("dds", "idle", "cpu")
assert all(item.type in plot_types for item in items)
fig: Figure
fig, axes = plt.subplots(1, 3, num="E2E type breakdown histograms")
fig.suptitle("E2E Latency Breakdown by Resource Type")
for type, ax in zip(plot_types, axes):
ax: Axes
durations = [item.duration for item in items if item.type == type]
ax.set_title(type)
ax.hist(durations)
ax.set_xlabel("Duration [s]")
ax.set_ylabel("Occurrences")
return fig
def e2e_breakdown_inst_stack(*paths: List[E2EBreakdownItem]):
fig: Figure
ax: Axes
fig, ax = plt.subplots(num="E2E instance breakdown stackplot")
fig.suptitle("Detailed E2E Latency Path Breakdown")
bottom = 0
for i in range(len(paths)):
e2e_items = [path[i] for path in paths]
durations = np.array([item.duration for item in e2e_items])
ax.bar(range(len(paths)), durations, bottom=bottom)
bottom = durations + bottom
return fig

View file

@ -0,0 +1,20 @@
from collections import namedtuple
E2EBreakdownItem = namedtuple("E2EBreakdownItem", ("type", "duration", "location"))
DepTree = namedtuple("DepTree", ("head", "deps"))
def depth(tree: DepTree):
return 1 + max(map(depth, tree.deps), default=0)
def size(tree: DepTree):
return 1 + sum(map(size, tree.deps))
def fanout(tree: DepTree):
if not tree.deps:
return 1
return sum(map(fanout, tree.deps))

File diff suppressed because it is too large Load diff

View file

@ -1,3 +1,4 @@
from uuid import UUID, uuid4
from collections import namedtuple, UserList, defaultdict from collections import namedtuple, UserList, defaultdict
from dataclasses import dataclass, field from dataclasses import dataclass, field
from functools import cached_property from functools import cached_property
@ -75,6 +76,7 @@ class TrContext:
publish_instances: Index['TrPublishInstance'] publish_instances: Index['TrPublishInstance']
callback_instances: Index['TrCallbackInstance'] callback_instances: Index['TrCallbackInstance']
topics: Index['TrTopic'] topics: Index['TrTopic']
_uuid: UUID
def __init__(self, handler: Ros2Handler): def __init__(self, handler: Ros2Handler):
print("[TrContext] Processing ROS 2 objects from traces...") print("[TrContext] Processing ROS 2 objects from traces...")
@ -110,6 +112,14 @@ class TrContext:
self.topics = Index((TrTopic(name=name, _c=self) for name in _unique_topic_names), self.topics = Index((TrTopic(name=name, _c=self) for name in _unique_topic_names),
name=False) name=False)
self._uuid = uuid4()
def __hash__(self):
return hash(self._uuid)
def __eq__(self, other):
return isinstance(other, TrContext) and other._uuid == self._uuid
def __repr__(self): def __repr__(self):
return f"TrContext" return f"TrContext"