Files
metin2-client/tools/perf/run_render_isolation.py

84 lines
3.9 KiB
Python

# coding: utf-8
"""Run opt-in single-section render isolation on a connected debug Android APK.
Requires its Client resources to be synced. Writes CSVs/screenshots/summary locally.
Normal startup is restored afterwards; device system settings are not changed.
"""
import argparse
import csv
import json
import shlex
import statistics
import subprocess
import time
from pathlib import Path
PKG = 'org.metin2port.client'
PERF = '/sdcard/Android/data/' + PKG + '/files/perf/'
def adb(*args):
return subprocess.check_output(['adb', *args], stderr=subprocess.STDOUT)
def shell(command):
return adb('shell', command).decode(errors='replace').strip()
def files():
return set(shell('ls ' + PERF + 'perf-*.csv 2>/dev/null || true').splitlines())
def parse(text):
return list(csv.DictReader(line for line in text.splitlines() if line and not line.startswith('#')))
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('output', type=Path)
parser.add_argument('--sections', default='baseline,terrain,characters,effects,touch,scene,baseline_repeat')
parser.add_argument('--windows', type=int, default=6)
parser.add_argument('--render-scale', type=float, default=0.6)
parser.add_argument('--msaa', type=int, choices=[1,2,4,8], default=1)
parser.add_argument('--fps-cap', type=int, default=30)
args = parser.parse_args()
args.output.mkdir(parents=True, exist_ok=True)
results = []
try:
for section in args.sections.split(','):
before = files()
launch = ('--interactive --fake-mobs 24 --perf-log --show-fps --fps-cap %d --msaa %d --render-scale %g'
% (args.fps_cap, args.msaa, args.render_scale))
if not section.startswith('baseline'):
launch += ' --diagnostic-skip-render ' + section
shell('am force-stop ' + PKG)
shell('am start -n ' + PKG + '/.MainActivity --es args ' + shlex.quote(launch))
print('START ' + section, flush=True)
deadline = time.monotonic() + 100
path = None
rows = []
while time.monotonic() < deadline:
time.sleep(2)
candidates = files() - before
if candidates:
path = sorted(candidates)[-1]
raw = adb('exec-out', 'cat', path).decode(errors='replace')
rows = [r for r in parse(raw) if r.get('map') == 'metin2_map_a1' and r.get('actors') == '28']
# Exclude the first interval after loading, then collect the requested count.
if len(rows) >= args.windows + 1:
break
if len(rows) < args.windows + 1:
raise RuntimeError('insufficient valid game samples: ' + section)
rows = rows[1:args.windows+1]
(args.output/(section+'.csv')).write_bytes(adb('exec-out', 'cat', path))
(args.output/(section+'.png')).write_bytes(adb('exec-out','screencap','-p'))
(args.output/(section+'-memory.txt')).write_text(shell('dumpsys meminfo '+PKG))
result = {'section':section,'args':launch,'device_csv':path,'windows':len(rows)}
for field in ['fps','gpu_ms','app_ms','draws','vertices','frame_p95_ms','rss_mb','batt_c']:
values = [float(r[field]) for r in rows]
result[field] = {'mean':statistics.mean(values),'min':min(values),'max':max(values)}
results.append(result)
(args.output/'summary.json').write_text(json.dumps(results,indent=2)+'\n')
print('DONE %s fps=%.2f gpu=%.1fms draws=%.0f batt=%.1fC' %
(section,result['fps']['mean'],result['gpu_ms']['mean'],result['draws']['mean'],result['batt_c']['mean']),flush=True)
finally:
shell('am force-stop ' + PKG)
shell('am start -n ' + PKG + '/.MainActivity')
if __name__ == '__main__':
main()