# 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()