#!/usr/bin/env python3 """Summarises a native client --perf-log CSV (native_render/perf_log.h). usage: perf_summary.py perf-YYYYMMDD-HHMMSS.csv [--all] Only in-game rows (a map is loaded) are summarised unless --all is given. Prints the median of each frame-time section so the largest cost stands out, the worst windows, thermal/clock trend and the "# hitch" lines. """ import csv import statistics import sys SECTIONS = [ ("frame_ms", "whole frame"), ("app_ms", " script thread (AppFrame)"), ("net_ms", " network"), ("camera_ms", " camera/resource"), ("update_game_ms", " UpdateGame"), ("render_game_ms", " RenderGame"), ("shadow_raster_ms", " CPU shadow raster"), ("py_ms", " Python callbacks (incl. Update/RenderGame)"), ("handoff_ms", " baton handoff"), ("pace_ms", " fps-cap sleep (before the frame)"), ("host_ms", " host UI/audio"), ("render_ms", " renderer"), ("sync_ms", " fence wait + acquire"), ("fence_ms", " frame-slot fence wait"), ("prepare_ms", " prepare (incl. uploads)"), ("geo_upload_ms", " geometry uploads"), ("tex_upload_ms", " texture uploads"), ("submit_ms", " submit"), ("present_ms", " present"), ("gpu_ms", " GPU (timestamps)"), ] def num(row, key): try: return float(row.get(key, "") or "nan") except ValueError: return float("nan") def median(rows, key): values = [num(r, key) for r in rows] values = [v for v in values if v == v] return statistics.median(values) if values else float("nan") def main(): args = [a for a in sys.argv[1:] if not a.startswith("--")] if not args: sys.exit(__doc__) path = args[0] comments, lines = [], [] with open(path, encoding="utf-8", errors="replace") as f: for line in f: (comments if line.startswith("#") else lines).append(line) rows = list(csv.DictReader(lines)) if "--all" not in sys.argv: rows = [r for r in rows if r.get("map")] print(path) for c in comments[:2]: print(c.rstrip()) if not rows: print("no in-game rows (use --all to include the login/select screens)") return duration = num(rows[-1], "t_s") - num(rows[0], "t_s") print("%d windows, %.0f s in game, maps: %s" % (len(rows), duration, ", ".join(sorted({r["map"] for r in rows})))) print("display %s Hz app vsync (render_hz) median %.1f fps cap %s" % ( "/".join(sorted({r.get("refresh_hz", "?") for r in rows})), median(rows, "render_hz"), rows[-1].get("fps_cap", "?"))) print("median fps %.1f frame p95 %.1f ms jank>33ms %d frames jank>50ms %d frames" % ( median(rows, "fps"), median(rows, "frame_p95_ms"), sum(int(num(r, "jank33")) for r in rows), sum(int(num(r, "jank50")) for r in rows))) print("median actors %.0f draws %.0f skinned %.0f python calls/frame %.0f (missing %.0f)" % ( median(rows, "actors"), median(rows, "draws"), median(rows, "skinned_draws"), median(rows, "py_calls"), median(rows, "py_missing"))) print("uploads/frame: geometry %.1f (%.1f KB) texture %.2f (%.0f KB)" % ( median(rows, "geo_uploads"), median(rows, "geo_kb"), median(rows, "tex_uploads"), median(rows, "tex_kb"))) print() print("median ms per frame") for key, label in SECTIONS: print(" %-48s %7.2f" % (label, median(rows, key))) print() print("worst windows (lowest fps)") for r in sorted(rows, key=lambda r: num(r, "fps"))[:5]: print(" t=%6.0fs fps=%5.1f frame=%5.1f app=%5.1f render_game=%5.1f render=%5.1f sync=%5.1f gpu=%5.1f " "actors=%s thermal=%s cpu_mhz=%s" % ( num(r, "t_s"), num(r, "fps"), num(r, "frame_ms"), num(r, "app_ms"), num(r, "render_game_ms"), num(r, "render_ms"), num(r, "sync_ms"), num(r, "gpu_ms"), r.get("actors"), r.get("thermal"), r.get("cpu_mhz"))) step = max(1, len(rows) // 8) print() print("trend (thermal: 0 none .. 3 severe; -1 unknown)") for r in rows[::step]: print(" t=%6.0fs fps=%5.1f thermal=%s batt=%s C cpu_mhz=%s (max %s) rss=%s MB" % ( num(r, "t_s"), num(r, "fps"), r.get("thermal"), r.get("batt_c"), r.get("cpu_mhz"), r.get("cpu_max_mhz", "?"), r.get("rss_mb"))) hitches = [c.rstrip() for c in comments if c.startswith("# hitch")] if hitches: print() print("%d hitches >= 100 ms (first 10)" % len(hitches)) for h in hitches[:10]: print(" " + h[2:]) if __name__ == "__main__": main()