blob: b85ba875c5e8fa1698318c1c1b144fd04204a484 [file] [edit]
#!/usr/bin/env python3
"""
Copyright 2025 Hans-Kristian Arntzen for Valve Corporation
This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
"""
"""
Compares two timestamp profiles
"""
import sys
import os
import argparse
import collections
import struct
import csv
ProfileCase = collections.namedtuple('ProfileCase', 'type hashes total_time non_ps_invocations ps_invocations count root_signature_hash')
ProfileMeta = collections.namedtuple('ProfileMeta', 'psos frame_count')
def split_hashes(hash_str):
return hash_str.split('+')
def read_csv(path):
frame_count = 0
psos = {}
with open(path, 'r') as csvfile:
reader = csv.reader(csvfile)
for row in reader:
if row[0] == 'PSO Type':
continue
if row[1] == 'SWAPCHAIN':
frame_count = int(row[6])
continue
if int(row[4]) == 0 and int(row[5]) == 0:
continue
psos[row[1]] = ProfileCase(row[0], split_hashes(row[2]), float(row[3]), int(row[4]), int(row[5]), int(row[6]), row[7] if len(row) >= 8 else 0)
if frame_count == 0:
raise AssertionError('Expected at least one row with SWAPCHAIN count != 0')
return ProfileMeta(psos, frame_count)
def report_list(cmp_list, first_lookup, second_lookup):
for v in cmp_list:
ratio = v[2] / v[1]
print(' ====', v[0], '====')
print(' Delta first -> second: {:.3f} %'.format(100.0 * (ratio - 1)))
print(' Type: {}'.format(first_lookup[v[0]].type))
print(' Total time: ({:.3f} us) vs ({:.3f} us)'.format(first_lookup[v[0]].total_time * 1e6, second_lookup[v[0]].total_time * 1e6))
print(' Total non-PS invocations: ({}) vs ({})'.format(first_lookup[v[0]].non_ps_invocations, second_lookup[v[0]].non_ps_invocations))
print(' Total PS invocations: ({}) vs ({})'.format(first_lookup[v[0]].ps_invocations, second_lookup[v[0]].ps_invocations))
print(' Total draws/dispatches: ({}) vs ({})'.format(first_lookup[v[0]].count, second_lookup[v[0]].count))
print(' Shader hashes:')
for hash in first_lookup[v[0]].hashes:
print(' {}'.format(hash))
print(' RootSig: {}'.format(first_lookup[v[0]].root_signature_hash))
def main():
parser = argparse.ArgumentParser(description = 'Script for parsing profiling data.')
parser.add_argument('--first', type = str, help = 'The first CSV.')
parser.add_argument('--second', help = 'The second CSV.')
parser.add_argument('--count', type = int, default = 10, help = 'Only list top --count entries per type.')
parser.add_argument('--type', type = str, help = 'Filter on PSO type.')
parser.add_argument('--threshold', type = float, default = 0.0, help = 'Only include entries which consume at least (seconds).')
args = parser.parse_args()
if not args.first:
raise AssertionError('Need --first.')
if not args.second:
raise AssertionError('Need --second.')
first_csv = read_csv(args.first)
second_csv = read_csv(args.second)
per_invocation_analysis = []
per_frame_analysis = []
per_dispatch_analysis = []
total_time_analysis = []
for pso_entry in second_csv.psos:
if pso_entry in first_csv.psos:
first = first_csv.psos[pso_entry]
second = second_csv.psos[pso_entry]
if args.type and first.type != args.type:
continue
if first.total_time < args.threshold or second.total_time < args.threshold:
continue
first_time_per_invocation = first.total_time / (first.non_ps_invocations + first.ps_invocations)
second_time_per_invocation = second.total_time / (second.non_ps_invocations + second.ps_invocations)
first_time_per_frame = first.total_time / first_csv.frame_count
second_time_per_frame = second.total_time / second_csv.frame_count
first_time_per_count = first.total_time / first.count
second_time_per_count = second.total_time / second.count
per_invocation_analysis.append((pso_entry, first_time_per_invocation, second_time_per_invocation))
per_frame_analysis.append((pso_entry, first_time_per_frame, second_time_per_frame))
per_dispatch_analysis.append((pso_entry, first_time_per_count, second_time_per_count))
total_time_analysis.append((pso_entry, first.total_time, second.total_time))
per_invocation_analysis.sort(reverse = True, key = lambda e: e[2] / e[1])
per_frame_analysis.sort(reverse = True, key = lambda e: e[2] / e[1])
per_dispatch_analysis.sort(reverse = True, key = lambda e: e[2] / e[1])
total_time_analysis_first = total_time_analysis[:]
total_time_analysis_second = total_time_analysis[:]
total_time_analysis_first.sort(reverse = True, key = lambda e: e[1])
total_time_analysis_second.sort(reverse = True, key = lambda e: e[2])
print('Total frames for first CSV:', first_csv.frame_count)
print('Total frames for second CSV:', second_csv.frame_count)
print('\nPer Invocation Analysis:')
report_list(per_invocation_analysis[0 : args.count], first_csv.psos, second_csv.psos)
print('\nPer Frame Analysis:')
report_list(per_frame_analysis[0 : args.count], first_csv.psos, second_csv.psos)
print('\nPer Dispatch Analysis:')
report_list(per_frame_analysis[0 : args.count], first_csv.psos, second_csv.psos)
print('\nMaximal Time Contribution Analysis (first):')
report_list(total_time_analysis_first[0 : args.count], first_csv.psos, second_csv.psos)
print('\nMaximal Time Contribution Analysis (second):')
report_list(total_time_analysis_second[0 : args.count], first_csv.psos, second_csv.psos)
if __name__ == '__main__':
main()