mirror of
https://github.com/mitch030504/AstroQuest-Frame.git
synced 2026-10-17 17:01:09 +02:00
137 lines
5.9 KiB
Python
137 lines
5.9 KiB
Python
#!/usr/bin/env python3
|
|
"""Sums up where the GPU's time goes from a Turnip trace.
|
|
|
|
The driver writes the trace when the core runs with
|
|
MESA_GPU_TRACES=print MESA_GPU_TRACEFILE=<file>
|
|
(MESA_GPU_TRACEFILE selects the output file). Every line is an event with the time
|
|
the GPU reached it, in nanoseconds. This groups the render passes by what they draw to and how
|
|
the driver chose to render them, and prints the milliseconds each group costs per frame.
|
|
|
|
gputrace.py <trace> [frames per second] [--passes]
|
|
|
|
Without a frame rate the figures are per second of trace. --passes lists the passes of one
|
|
frame in order.
|
|
"""
|
|
import re
|
|
import sys
|
|
from collections import defaultdict
|
|
|
|
LINE = re.compile(r'^(\d+)\s+[+-]\d+: (\w+)(?:: (.*))?$')
|
|
|
|
|
|
def fields(text):
|
|
result = {}
|
|
for part in (text or '').split(', '):
|
|
if '=' in part:
|
|
name, value = part.split('=', 1)
|
|
result[name.strip()] = value.strip()
|
|
return result
|
|
|
|
|
|
def main():
|
|
args = [a for a in sys.argv[1:] if not a.startswith('--')]
|
|
list_passes = '--passes' in sys.argv
|
|
path = args[0]
|
|
fps = float(args[1]) if len(args) > 1 else None
|
|
|
|
events = []
|
|
with open(path, encoding='utf-8', errors='replace') as handle:
|
|
# A trace cut out of a longer one begins in the middle of a line.
|
|
handle.readline()
|
|
for line in handle:
|
|
match = LINE.match(line.rstrip())
|
|
if match:
|
|
events.append((int(match.group(1)), match.group(2), fields(match.group(3))))
|
|
if not events:
|
|
print('no events')
|
|
return
|
|
|
|
passes = []
|
|
current = None
|
|
others = defaultdict(lambda: [0, 0])
|
|
open_events = {}
|
|
for ts, name, info in events:
|
|
if name == 'start_render_pass':
|
|
current = {'start': ts, 'info': info, 'parts': defaultdict(int), 'tiles': 0}
|
|
elif name == 'end_render_pass':
|
|
if current is not None:
|
|
current['end'] = ts
|
|
current['result'] = info
|
|
passes.append(current)
|
|
current = None
|
|
elif name.startswith('start_'):
|
|
open_events[name[6:]] = ts
|
|
if name == 'start_draw_ib_gmem' and current is not None:
|
|
current['tiles'] += 1
|
|
elif name.startswith('end_'):
|
|
kind = name[4:]
|
|
start = open_events.pop(kind, None)
|
|
if start is None:
|
|
continue
|
|
# Stores are stamped out of order (their end is taken before the blit runs).
|
|
length = max(ts - start, 0)
|
|
if current is not None:
|
|
current['parts'][kind] += length
|
|
elif kind not in ('cmd_buffer',):
|
|
others[kind][0] += 1
|
|
others[kind][1] += length
|
|
|
|
first = min(e[0] for e in events)
|
|
last = max(e[0] for e in events)
|
|
seconds = (last - first) / 1e9
|
|
frames = fps * seconds if fps else seconds
|
|
unit = 'frame' if fps else 'second'
|
|
print(f'{len(events)} events over {seconds:.2f} s, {len(passes)} render passes'
|
|
f' ({len(passes) / frames:.1f} per {unit})')
|
|
|
|
groups = defaultdict(lambda: {'count': 0, 'time': 0, 'draws': 0, 'parts': defaultdict(int)})
|
|
busy = 0
|
|
for p in passes:
|
|
info, result = p['info'], p['result']
|
|
tiled = result.get('tiledRender') == 'true'
|
|
reason = result.get('tilingDisableReason', '')
|
|
key = (f"{info.get('width')}x{info.get('height')}",
|
|
f"{info.get('attachment_count')} att" + (' +depth' if info.get('hasDepth') == 'true' else ''),
|
|
f"load {info.get('loadCPP')} store {info.get('storeCPP')} clear {info.get('clearCPP')}",
|
|
'tiled' if tiled else 'direct' + (f' ({reason})' if reason else ''))
|
|
group = groups[key]
|
|
length = p['end'] - p['start']
|
|
busy += length
|
|
group['count'] += 1
|
|
group['time'] += length
|
|
group['draws'] += int(result.get('drawCount', 0))
|
|
for kind, value in p['parts'].items():
|
|
group['parts'][kind] += value
|
|
|
|
print(f'time inside render passes: {busy / 1e6 / frames:.2f} ms per {unit}')
|
|
print(f'{"ms":>8} {"passes":>7} {"draws":>7} {"ms each":>8} what')
|
|
for key, group in sorted(groups.items(), key=lambda item: -item[1]['time']):
|
|
parts = ', '.join(f'{kind} {value / 1e6 / frames:.2f}'
|
|
for kind, value in sorted(group['parts'].items(), key=lambda i: -i[1])
|
|
if value / 1e6 / frames >= 0.05)
|
|
print(f"{group['time'] / 1e6 / frames:8.2f} {group['count'] / frames:7.1f}"
|
|
f" {group['draws'] / frames:7.1f} {group['time'] / 1e6 / group['count']:8.3f}"
|
|
f" {' | '.join(key)}" + (f' [{parts}]' if parts else ''))
|
|
for kind, (count, length) in sorted(others.items(), key=lambda item: -item[1][1]):
|
|
print(f'{length / 1e6 / frames:8.2f} {count / frames:7.1f} outside passes: {kind}')
|
|
|
|
if list_passes:
|
|
# One frame's worth, starting at the first pass after the middle of the trace.
|
|
per_frame = max(int(round(len(passes) / frames)), 1) if fps else 60
|
|
begin = len(passes) // 2
|
|
previous_end = None
|
|
for p in passes[begin:begin + per_frame]:
|
|
info, result = p['info'], p['result']
|
|
gap = (p['start'] - previous_end) / 1e6 if previous_end is not None else 0.0
|
|
previous_end = p['end']
|
|
tiled = result.get('tiledRender') == 'true'
|
|
print(f" +{gap:6.3f} {(p['end'] - p['start']) / 1e6:7.3f} ms {info.get('width')}x{info.get('height')}"
|
|
f" att {info.get('attachment_count')} depth {info.get('hasDepth')}"
|
|
f" load {info.get('loadCPP')} store {info.get('storeCPP')} clear {info.get('clearCPP')}"
|
|
f" draws {result.get('drawCount')} {'tiled x' + str(p['tiles']) if tiled else 'direct'}"
|
|
f" lrz {result.get('lrzStatus')} {result.get('tilingDisableReason', '')}")
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|