Files
mtgodot-poc/tools/perf_summary.py
T
shenleiandClaude Opus 5.5 82503315c6 native: frame-rate setting, idle frame rate and battery power telemetry
- MtFrameRate setting (30 / 60 default / display max) as app.Get/SetFrameRateMode,
  with a "画面帧率" row injected into the system option dialog (mt_framerate); saved
  in the SDL pref path.
- FrameRatePolicy in the live loop: 30 fps after 15 s without input (not while auto
  hunting), display switched to 60 Hz unless the highest mode, ADPF
  setPreferPowerEfficiency, vsync pacing when the display already runs at the target.
  --fps-mode / --idle-fps / --idle-after; --fps-cap / --refresh-rate still pin a test run.
- Perf log: battery current/voltage/power, plugged, fps_mode, idle columns;
  perf_summary groups power by mode.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 18:14:19 +09:00

127 lines
5.4 KiB
Python

#!/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)))
if "power_mw" in rows[0]:
# power_mw is -1 in a window with any sample on the charger: the battery current is then the
# charger's surplus, not the app's draw. Split by frame-rate mode and idle.
print("power (battery, unplugged windows only; plugged windows show current only)")
groups = {}
for r in rows:
groups.setdefault((r.get("fps_mode", "?"), r.get("idle", "?")), []).append(r)
for (mode, idle), rs in sorted(groups.items()):
powered = [r for r in rs if num(r, "power_mw") >= 0]
print(" mode %s%s: %d windows fps %.1f %s current %.0f mA" % (
mode, " idle" if idle == "1" else "", len(rs), median(rs, "fps"),
("power %.0f mW (%d windows)" % (median(powered, "power_mw"), len(powered))) if powered
else "power n/a (plugged)", median(rs, "batt_ma")))
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()