| #!/usr/bin/env python3 |
| # Copyright (c) 2025 The Khronos Group Inc. |
| # Copyright (c) 2025 Valve Corporation |
| # Copyright (c) 2025 LunarG, Inc. |
| # |
| # Licensed under the Apache License, Version 2.0 (the "License"); |
| # you may not use this file except in compliance with the License. |
| # You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| |
| import argparse |
| import os |
| import re |
| import sys |
| import struct |
| import numpy as np |
| from collections import defaultdict |
| |
| def PrintExecutionModels(execution_models: set): |
| names = [] |
| for e in execution_models: |
| if e == 0: # Maps to ExecutionModelVertex |
| names.append('Vertex') |
| elif e == 1: |
| names.append('TessControl') |
| elif e == 2: |
| names.append('TessEval') |
| elif e == 3: |
| names.append('Geometry') |
| elif e == 4: |
| names.append('Fragment') |
| elif e == 5: |
| names.append('Compute') |
| elif e == 6: |
| names.append('Kernel') # not allowed |
| elif e == 5267: |
| names.append('TaskNV') |
| elif e == 5268: |
| names.append('MeshNV') |
| elif e == 5364: |
| names.append('TaskEXT') |
| elif e == 5365: |
| names.append('MeshEXT') |
| elif e == 5313: |
| names.append("RayGenerationKHR") |
| elif e == 5314: |
| names.append("IntersectionKHR") |
| elif e == 5315: |
| names.append("AnyHitKHR") |
| elif e == 5316: |
| names.append("ClosestHitKHR") |
| elif e == 5317: |
| names.append("MissKHR") |
| elif e == 5318: |
| names.append("CallableKHR") |
| else: |
| print("ERROR - Stage not found") |
| return ",".join(names) |
| |
| def ExtractShaderId(path: str): |
| match = re.search(r'dump_(\d+)_after\.spv', path) |
| return int(match.group(1)) if match else None |
| |
| def ParseSpirv(file: str, stats_collector: dict): |
| with open(file, "rb") as f: |
| # Read the whole binary file as uint32 words |
| words = list(struct.unpack(f"{len(f.read()) // 4}I", f.seek(0) or f.read())) |
| |
| name_dict = dict() |
| function_call_count = defaultdict(int) |
| execution_models = set() # Use a set to store unique numbers |
| |
| offset = 5 # First 5 words are the header |
| |
| while offset < len(words): |
| instruction = words[offset] |
| length = instruction >> 16 |
| opcode = instruction & 0xFFFF |
| |
| if opcode == 5: # OpName |
| target_id = words[offset + 1] |
| raw_string = words[offset + 2 : offset + length] |
| byte_data = b''.join(struct.pack("I", word) for word in raw_string) |
| name = byte_data.split(b'\x00', 1)[0].decode("utf-8") |
| name_dict[target_id] = name |
| |
| elif opcode == 15: # OpEntryPoint |
| execution_model = words[offset + 1] |
| execution_models.add(execution_model) |
| |
| elif opcode == 57: # OpFunctionCall |
| function_id = words[offset + 3] |
| function_call_count[function_id] += 1 |
| |
| offset += length # Move to the next instruction |
| |
| print(f'Shader Id {ExtractShaderId(file)} ({PrintExecutionModels(execution_models)})') |
| for function_id, count in function_call_count.items(): |
| # Non-instrumented functions might not have a name |
| if function_id in name_dict: |
| function_name = name_dict[function_id] |
| if function_name.startswith('inst_'): |
| print(f" {function_name}: {count}") |
| # Store count per function to later do statistics |
| if function_name not in stats_collector: |
| stats_collector[function_name] = [] |
| stats_collector[function_name].append(count) |
| |
| def print_statistics(stats_collector: dict): |
| print("\n=== Function Call Statistics ===") |
| for function_name, counts in stats_collector.items(): |
| counts_array = np.array(counts) |
| print(f"\nFunction: {function_name}") |
| print(f" Count: {len(counts)}") |
| print(f" Min: {np.min(counts_array)}") |
| print(f" Max: {np.max(counts_array)}") |
| print(f" Mean: {np.mean(counts_array):.2f}") |
| print(f" Median: {np.median(counts_array):.2f}") |
| print(f" 75th percentile: {np.percentile(counts_array, 75):.2f}") |
| print(f" 95th percentile: {np.percentile(counts_array, 95):.2f}") |
| |
| def main(argv): |
| parser = argparse.ArgumentParser(description='Get info about a GPU-AV instrumented SPIR-V dump') |
| group = parser.add_mutually_exclusive_group(required=True) |
| group.add_argument('--files', nargs='+', help='List of files to inspect') |
| group.add_argument('--dir', help='Pass in a directory path that contains all the dumped files.', dest='directory') |
| args = parser.parse_args(argv) |
| |
| spirv_list = [] |
| if args.directory: |
| after_files = [] |
| pattern = re.compile(r'dump_(\d+)_after') |
| for root, _, files in os.walk(args.directory): |
| for file in files: |
| if pattern.search(file): |
| full_path = os.path.join(root, file) |
| after_files.append(full_path) |
| |
| spirv_list = sorted(after_files, key=ExtractShaderId) |
| else: |
| spirv_list = args.files |
| |
| stats_collector = defaultdict(list) |
| |
| for file in spirv_list: |
| ParseSpirv(file, stats_collector) |
| |
| print_statistics(stats_collector) |
| |
| if __name__ == '__main__': |
| sys.exit(main(sys.argv[1:])) |