diff --git a/lglpy/timeline/data/processed_trace.py b/lglpy/timeline/data/processed_trace.py index ba3b108..73e7366 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[str, str] = 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,9 @@ 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 +400,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 +411,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 +422,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 +433,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 +444,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 +459,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 +479,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 +490,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 +511,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 +529,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 +589,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 +605,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 +618,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 +678,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 +694,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 +706,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 +763,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 +781,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 +847,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 +865,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 +936,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 +954,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 +1017,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 +1035,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..9cc0411 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[str, str] = 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()