From 431c4040a15e062c5bda2ce9b2eac30d2dbe3d49 Mon Sep 17 00:00:00 2001 From: Peter Harris Date: Wed, 13 Aug 2025 17:18:20 +0100 Subject: [PATCH 1/3] Timeline view: Add string memoization --- lglpy/timeline/data/processed_trace.py | 115 ++++++++++++++++++------- lglpy/timeline/data/raw_trace.py | 57 ++++++++++-- lglpy/timeline/gui/timeline/view.py | 1 + lglpy/timeline/gui/window.py | 1 - 4 files changed, 136 insertions(+), 38 deletions(-) diff --git a/lglpy/timeline/data/processed_trace.py b/lglpy/timeline/data/processed_trace.py index ba3b108..0e0c551 100644 --- a/lglpy/timeline/data/processed_trace.py +++ b/lglpy/timeline/data/processed_trace.py @@ -61,6 +61,31 @@ class GPUWorkload: PARENS = re.compile(r'(\(.*\))') RESOLUTION = re.compile(r'\d+x\d+') WHITESPACE = re.compile(r'\s\s+') + MEMO = dict() + + @classmethod + def memoize(cls, string: str) -> str: + ''' + Get a memoized version of a string to reduce runtime memory use and + improve rendering performance. + + Args: + string: User string to memoize. + + Return: + Memoized copy of a string. + ''' + memo = GPUWorkload.MEMO + if string not in memo: + memo[string] = string + return memo[string] + + @classmethod + def clear_memoize_cache(cls) -> None: + ''' + Clear the local memoization cache. + ''' + GPUWorkload.MEMO.clear() def __init__( self, event: RenderstageEvent, metadata: Optional[MetadataWork]): @@ -105,7 +130,8 @@ def get_label_name_full(self) -> Optional[str]: return None if not LABEL_HEURISTICS: - self.parsed_label_name_full = self.label_stack[-1] + label = GPUWorkload.memoize(self.label_stack[-1]) + self.parsed_label_name_full = label return self.parsed_label_name_full # Create a copy we can edit ... @@ -146,6 +172,7 @@ def get_label_name_full(self) -> Optional[str]: else: label = '.'.join(labels) + label = GPUWorkload.memoize(label) self.parsed_label_name_full = label return self.parsed_label_name_full @@ -181,6 +208,7 @@ def get_label_name(self) -> Optional[str]: postfix = label[-half_max:] label = f'{prefix}...{postfix}' + label = GPUWorkload.memoize(label) self.parsed_label_name = label return self.parsed_label_name @@ -239,7 +267,8 @@ def get_long_label(self) -> str: ''' # Subclass will override this if metadata exists # Submit ID isn't useful, but traces back to Perfetto data for debug - return f'Submit: {self.submit_id}' + label = f'Submit: {self.submit_id}' + return GPUWorkload.memoize(label) def get_short_label(self) -> str: ''' @@ -250,7 +279,8 @@ def get_short_label(self) -> str: ''' # Subclass will override this if metadata exists # Submit ID isn't useful, but traces back to Perfetto data for debug - return f'Submit: {self.submit_id}' + label = f'Submit: {self.submit_id}' + return GPUWorkload.memoize(label) def get_key_value_properties(self) -> dict[str, str]: ''' @@ -353,7 +383,10 @@ def get_resolution_str(self) -> str: Returns: Returns the label for use in the UI. ''' - return f'{self.width}x{self.height}' + label = f'{self.width}x{self.height}' + label = self.memoize(label) + return label + def get_draw_count_str(self) -> str: ''' @@ -368,7 +401,8 @@ def get_draw_count_str(self) -> str: if self.draw_call_count == 1: return '1 draw' - return f'{self.draw_call_count} draws' + label = f'{self.draw_call_count} draws' + return self.memoize(label) def get_subpass_count_str(self) -> str: ''' @@ -378,7 +412,8 @@ def get_subpass_count_str(self) -> str: Returns the label for use in the UI. ''' es = '' if self.subpass_count == 1 else 'es' - return f'{self.subpass_count} subpass{es}' + label = f'{self.subpass_count} subpass{es}' + return self.memoize(label) def get_attachment_present_str(self) -> str: ''' @@ -388,7 +423,8 @@ def get_attachment_present_str(self) -> str: Returns the label for use in the UI. ''' bindings = [x.binding for x in self.attachments] - return self.get_compact_string(bindings) + label = self.get_compact_str(bindings) + return GPUWorkload.memoize(label) def get_attachment_loadop_str(self) -> str: ''' @@ -398,7 +434,8 @@ def get_attachment_loadop_str(self) -> str: Returns the label for use in the UI. ''' bindings = [x.binding for x in self.attachments if x.is_loaded] - return self.get_compact_string(bindings) + label = self.get_compact_str(bindings) + return GPUWorkload.memoize(label) def get_attachment_storeop_str(self) -> str: ''' @@ -408,10 +445,11 @@ def get_attachment_storeop_str(self) -> str: Returns the label for use in the UI. ''' bindings = [x.binding for x in self.attachments if x.is_stored] - return self.get_compact_string(bindings) + label = self.get_compact_str(bindings) + return GPUWorkload.memoize(label) @classmethod - def get_compact_string(cls, bindings: list[str]) -> str: + def get_compact_str(cls, bindings: list[str]) -> str: ''' Get the compact UI string for a set of attachment bind points. @@ -422,7 +460,8 @@ def get_compact_string(cls, bindings: list[str]) -> str: A binding string of the form, e.g. "C0124DS". ''' merge = ''.join(bindings) - return ''.join([j for i, j in enumerate(merge) if j not in merge[:i]]) + label = ''.join([j for i, j in enumerate(merge) if j not in merge[:i]]) + return GPUWorkload.memoize(label) def get_attachment_long_label(self) -> str: ''' @@ -441,7 +480,8 @@ def get_attachment_long_label(self) -> str: if stored: stored = f' > store({stored}) ' - return f'{loaded}[{present}]{stored}' + label = f'{loaded}[{present}]{stored}' + return GPUWorkload.memoize(label) def get_attachment_short_label(self) -> str: ''' @@ -451,7 +491,8 @@ def get_attachment_short_label(self) -> str: A string showing attachments without load/storeOp usage. ''' present = self.get_attachment_present_str() - return f'[{present}]' + label = f'[{present}]' + return GPUWorkload.memoize(label) def get_long_label(self) -> str: ''' @@ -471,7 +512,8 @@ def get_long_label(self) -> str: line = self.get_attachment_long_label() lines.append(line) - return '\n'.join(lines) + label = '\n'.join(lines) + return GPUWorkload.memoize(label) def get_short_label(self) -> str: ''' @@ -488,7 +530,8 @@ def get_short_label(self) -> str: line = self.get_attachment_short_label() lines.append(line) - return '\n'.join(lines) + label = '\n'.join(lines) + return GPUWorkload.memoize(label) class GPUDispatch(GPUWorkload): @@ -547,7 +590,8 @@ def get_resolution_str(self) -> str: if self.groups_z > 1: dims.append(self.groups_z) - return f'{"x".join([str(dim) for dim in dims])} groups' + label = f'{"x".join([str(dim) for dim in dims])} groups' + return GPUWorkload.memoize(label) def get_long_label(self) -> str: ''' @@ -562,7 +606,8 @@ def get_long_label(self) -> str: lines.append(label_name) lines.append(self.get_short_label()) - return '\n'.join(lines) + label = '\n'.join(lines) + return GPUWorkload.memoize(label) def get_short_label(self) -> str: ''' @@ -574,7 +619,8 @@ def get_short_label(self) -> str: lines = [] line = self.get_resolution_str() lines.append(line) - return '\n'.join(lines) + label = '\n'.join(lines) + return GPUWorkload.memoize(label) class GPUTraceRays(GPUWorkload): @@ -633,7 +679,8 @@ def get_resolution_str(self) -> str: if self.items_z > 1: dims.append(self.items_z) - return f'{"x".join([str(dim) for dim in dims])} items' + label = f'{"x".join([str(dim) for dim in dims])} items' + return GPUWorkload.memoize(label) def get_long_label(self) -> str: ''' @@ -648,7 +695,8 @@ def get_long_label(self) -> str: lines.append(label_name) lines.append(self.get_short_label()) - return '\n'.join(lines) + label = '\n'.join(lines) + return GPUWorkload.memoize(label) def get_short_label(self) -> str: ''' @@ -659,7 +707,8 @@ def get_short_label(self) -> str: ''' lines = [] lines.append(self.get_resolution_str()) - return '\n'.join(lines) + label = '\n'.join(lines) + return GPUWorkload.memoize(label) class GPUImageTransfer(GPUWorkload): @@ -715,7 +764,8 @@ def get_transfer_size_str(self) -> str: return f'? pixels' s = 's' if self.pixel_count != 1 else '' - return f'{self.pixel_count} pixel{s}' + label = f'{self.pixel_count} pixel{s}' + return GPUWorkload.memoize(label) def get_long_label(self) -> str: ''' @@ -732,7 +782,8 @@ def get_long_label(self) -> str: line = f'{self.transfer_type} ({self.get_transfer_size_str()})' lines.append(line) - return '\n'.join(lines) + label = '\n'.join(lines) + return GPUWorkload.memoize(label) def get_short_label(self) -> str: ''' @@ -797,7 +848,8 @@ def get_transfer_size_str(self) -> str: return f'? bytes' s = 's' if self.byte_count != 1 else '' - return f'{self.byte_count} byte{s}' + label = f'{self.byte_count} byte{s}' + return GPUWorkload.memoize(label) def get_long_label(self) -> str: ''' @@ -814,7 +866,8 @@ def get_long_label(self) -> str: line = f'{self.transfer_type} ({self.get_transfer_size_str()})' lines.append(line) - return '\n'.join(lines) + label = '\n'.join(lines) + return GPUWorkload.memoize(label) def get_short_label(self) -> str: ''' @@ -884,7 +937,8 @@ def get_transfer_size_str(self) -> str: return f'? primitives' s = 's' if self.primitive_count != 1 else '' - return f'{self.primitive_count} primitive{s}' + label = f'{self.primitive_count} primitive{s}' + return GPUWorkload.memoize(label) def get_long_label(self) -> str: ''' @@ -901,7 +955,8 @@ def get_long_label(self) -> str: line = f'{self.build_type} ({self.get_transfer_size_str()})' lines.append(line) - return '\n'.join(lines) + label = '\n'.join(lines) + return GPUWorkload.memoize(label) def get_short_label(self) -> str: ''' @@ -963,7 +1018,8 @@ def get_transfer_size_str(self) -> str: return f'? bytes' s = 's' if self.byte_count != 1 else '' - return f'{self.byte_count} byte{s}' + label = f'{self.byte_count} byte{s}' + return GPUWorkload.memoize(label) def get_long_label(self) -> str: ''' @@ -980,7 +1036,8 @@ def get_long_label(self) -> str: line = f'{self.transfer_type} ({self.get_transfer_size_str()})' lines.append(line) - return '\n'.join(lines) + label = '\n'.join(lines) + return GPUWorkload.memoize(label) def get_short_label(self) -> str: ''' diff --git a/lglpy/timeline/data/raw_trace.py b/lglpy/timeline/data/raw_trace.py index 3ebd70a..68b6bc6 100644 --- a/lglpy/timeline/data/raw_trace.py +++ b/lglpy/timeline/data/raw_trace.py @@ -161,7 +161,7 @@ def __init__(self, metadata: JSONType): Args: metadata: JSON payload from the layer. ''' - self.binding = str(metadata['binding']) + self.binding = MetadataWorkload.memoize(metadata['binding']) self.is_loaded = bool(metadata.get('load', False)) self.is_stored = bool(metadata.get('store', True)) self.is_resolved = bool(metadata.get('resolve', True)) @@ -300,9 +300,37 @@ class MetadataWorkload: label_stack: Debug label stack, or None if no user labels. ''' + MEMO = dict() + + @classmethod + def memoize(cls, string: str) -> str: + ''' + Get a memoized version of a string to reduce runtime memory use and + improve rendering performance. + + Args: + string: User string to memoize. + + Return: + Memoized copy of a string. + ''' + string = str(string) + memo = MetadataWorkload.MEMO + + if string not in memo: + memo[string] = string + return memo[string] + + @classmethod + def clear_memoize_cache(cls) -> None: + ''' + Clear the local memoization cache. + ''' + MetadataWorkload.MEMO.clear() + def __init__(self, submit: MetadataSubmit, metadata: JSONType): ''' - Parsed GPU Timeline layer payload for a single render pass. + Parsed GPU Timeline layer payload for a single workload. Args: submit: The submit information. @@ -310,8 +338,17 @@ def __init__(self, submit: MetadataSubmit, metadata: JSONType): ''' self.tag_id = int(metadata['tid']) self.submit = submit + self.label_stack = None - self.label_stack = metadata.get('label', None) + raw_labels = metadata.get('label', None) + if raw_labels is None: + return + + # Memoize the debug label stack as it tends to repeat + self.label_stack = [] + for label in raw_labels: + label = MetadataWorkload.memoize(label) + self.label_stack.append(label) def get_perfetto_tag_id(self) -> str: ''' @@ -439,7 +476,7 @@ def __init__(self, submit: MetadataSubmit, metadata: JSONType): ''' super().__init__(submit, metadata) - self.subtype = str(metadata['subtype']) + self.subtype = MetadataWorkload.memoize(metadata['subtype']) self.pixel_count = int(metadata['pixelCount']) @@ -462,7 +499,8 @@ def __init__(self, submit: MetadataSubmit, metadata: JSONType): ''' super().__init__(submit, metadata) - self.subtype = str(metadata['subtype']) + subtype = MetadataWorkload.memoize(metadata['subtype']) + self.subtype = subtype self.byte_count = int(metadata['byteCount']) @@ -485,7 +523,7 @@ def __init__(self, submit: MetadataSubmit, metadata: JSONType): ''' super().__init__(submit, metadata) - self.subtype = str(metadata['subtype']) + self.subtype = MetadataWorkload.memoize(metadata['subtype']) self.primitive_count = int(metadata['primitiveCount']) @@ -510,7 +548,7 @@ def __init__(self, submit: MetadataSubmit, metadata: JSONType): ''' super().__init__(submit, metadata) - self.subtype = str(metadata['subtype']) + self.subtype = MetadataWorkload.memoize(metadata['subtype']) self.byte_count = int(metadata['byteCount']) @@ -560,7 +598,7 @@ def __init__(self, start_time: int, spec: Any): if item.name != 'Labels': continue - self.user_label = str(item.value) + self.user_label = MetadataWorkload.memoize(item.value) # Helper for typing all workload subclasses of MetadataWorkload @@ -933,3 +971,6 @@ def __init__(self, trace_file: str, metadata_file: str): if metadata_file: self.metadata = \ self.load_metadata_from_file(metadata_file, start_time) + + # Clear the memoization cache as we don't need it any more + MetadataWorkload.clear_memoize_cache() diff --git a/lglpy/timeline/gui/timeline/view.py b/lglpy/timeline/gui/timeline/view.py index 077ac4e..99bd258 100644 --- a/lglpy/timeline/gui/timeline/view.py +++ b/lglpy/timeline/gui/timeline/view.py @@ -39,6 +39,7 @@ from ...drawable.drawable_channel import DrawableChannel from ...drawable.world_drawable import WorldDrawableRect from ...drawable.style import Style, StyleSet, StyleSetLibrary +from ...data.processed_trace import GPUWorkload class FakeMouseDrag: diff --git a/lglpy/timeline/gui/window.py b/lglpy/timeline/gui/window.py index da149d8..f5ac439 100644 --- a/lglpy/timeline/gui/window.py +++ b/lglpy/timeline/gui/window.py @@ -366,7 +366,6 @@ def deferred_load(): try: trace_data = RawTrace(trace_file, metadata_file) self.trace_data = GPUTrace(trace_data) - self.loaded_file_path = trace_file except Exception: self.status.log('Open cancelled (failed to load)') From 57abd2374698640d625f5a777486c22df451329d Mon Sep 17 00:00:00 2001 From: Peter Harris Date: Wed, 13 Aug 2025 17:50:35 +0100 Subject: [PATCH 2/3] Whitespace --- lglpy/timeline/data/processed_trace.py | 1 - lglpy/timeline/gui/timeline/view.py | 1 - lglpy/timeline/gui/window.py | 1 + 3 files changed, 1 insertion(+), 2 deletions(-) diff --git a/lglpy/timeline/data/processed_trace.py b/lglpy/timeline/data/processed_trace.py index 0e0c551..4bfea95 100644 --- a/lglpy/timeline/data/processed_trace.py +++ b/lglpy/timeline/data/processed_trace.py @@ -387,7 +387,6 @@ def get_resolution_str(self) -> str: label = self.memoize(label) return label - def get_draw_count_str(self) -> str: ''' Get the draw call count string. diff --git a/lglpy/timeline/gui/timeline/view.py b/lglpy/timeline/gui/timeline/view.py index 99bd258..077ac4e 100644 --- a/lglpy/timeline/gui/timeline/view.py +++ b/lglpy/timeline/gui/timeline/view.py @@ -39,7 +39,6 @@ from ...drawable.drawable_channel import DrawableChannel from ...drawable.world_drawable import WorldDrawableRect from ...drawable.style import Style, StyleSet, StyleSetLibrary -from ...data.processed_trace import GPUWorkload class FakeMouseDrag: diff --git a/lglpy/timeline/gui/window.py b/lglpy/timeline/gui/window.py index f5ac439..da149d8 100644 --- a/lglpy/timeline/gui/window.py +++ b/lglpy/timeline/gui/window.py @@ -366,6 +366,7 @@ def deferred_load(): try: trace_data = RawTrace(trace_file, metadata_file) self.trace_data = GPUTrace(trace_data) + self.loaded_file_path = trace_file except Exception: self.status.log('Open cancelled (failed to load)') From 93b18dfd640f40816e821f0df46650906a62edd4 Mon Sep 17 00:00:00 2001 From: Peter Harris Date: Wed, 13 Aug 2025 18:26:14 +0100 Subject: [PATCH 3/3] Mypy update --- lglpy/timeline/data/processed_trace.py | 2 +- lglpy/timeline/data/raw_trace.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lglpy/timeline/data/processed_trace.py b/lglpy/timeline/data/processed_trace.py index 4bfea95..73e7366 100644 --- a/lglpy/timeline/data/processed_trace.py +++ b/lglpy/timeline/data/processed_trace.py @@ -61,7 +61,7 @@ class GPUWorkload: PARENS = re.compile(r'(\(.*\))') RESOLUTION = re.compile(r'\d+x\d+') WHITESPACE = re.compile(r'\s\s+') - MEMO = dict() + MEMO: dict[str, str] = dict() @classmethod def memoize(cls, string: str) -> str: diff --git a/lglpy/timeline/data/raw_trace.py b/lglpy/timeline/data/raw_trace.py index 68b6bc6..9cc0411 100644 --- a/lglpy/timeline/data/raw_trace.py +++ b/lglpy/timeline/data/raw_trace.py @@ -300,7 +300,7 @@ class MetadataWorkload: label_stack: Debug label stack, or None if no user labels. ''' - MEMO = dict() + MEMO: dict[str, str] = dict() @classmethod def memoize(cls, string: str) -> str: