#!/usr/bin/env python3 """Desempenho de uma sessão a partir de um log do port (TLG_FRAME_STATS=1, uma amostra a cada 5 s): FPS, pior quadro e draws por quadro ao longo do tempo. Uso: session_chart.py [nome do jogo] [trava de FPS: 30 (padrão), 60 ou 0 = sem trava]""" import re import statistics import sys import matplotlib matplotlib.use('Agg') import matplotlib.pyplot as plt STAT = re.compile(r'Frame stats: ([\d.]+) FPS, worst frame ([\d.]+) ms.*?(\d+) draws/frame') PACE = re.compile(r'Frame pacing: median ([\d.]+) ms, stddev ([\d.]+) ms, p99 ([\d.]+) ms') text = open(sys.argv[1], encoding='utf-8', errors='replace').read() stats = [tuple(map(float, m.groups())) for m in STAT.finditer(text)] pacing = [tuple(map(float, m.groups())) for m in PACE.finditer(text)] # The first samples cover boot and shader warm-up; the chart shows the whole session anyway. t = [i * 5 / 60 for i in range(len(stats))] fps = [s[0] for s in stats] worst = [s[1] for s in stats] draws = [s[2] for s in stats] ink, muted, accent, warn = '#1f2937', '#6b7280', '#2563eb', '#dc2626' plt.rcParams.update({'font.size': 11, 'axes.edgecolor': '#d1d5db', 'axes.labelcolor': ink, 'xtick.color': muted, 'ytick.color': muted, 'axes.spines.top': False, 'axes.spines.right': False}) fig, (a, b, c) = plt.subplots(3, 1, figsize=(10, 6.4), sharex=True, gridspec_kw={'height_ratios': [1.1, 1, 0.8]}) fig.patch.set_facecolor('white') a.plot(t, fps, color=accent, lw=1.8) cap = float(sys.argv[4]) if len(sys.argv) > 4 else 30.0 if cap: a.axhline(cap, color=muted, lw=0.8, ls='--') a.set_ylim(0, cap * 1.2) a.text(t[-1], cap * 1.04, 'trava do jogo: %.0f FPS' % cap, ha='right', color=muted, fontsize=9) else: # Uncapped game (inFAMOUS): the axis follows the session, with 30 and 60 FPS for reference. a.set_ylim(0, max(66, max(fps) * 1.1)) for ref in (30, 60): a.axhline(ref, color=muted, lw=0.8, ls='--') a.text(t[-1], ref + 1, '%d FPS' % ref, ha='right', color=muted, fontsize=9) a.set_ylabel('FPS') ideal = 1000.0 / (cap or 30.0) b.plot(t, worst, color=warn, lw=1.4) b.axhline(ideal, color=muted, lw=0.8, ls='--') b.set_ylim(0, max(100, max(worst) * 1.1)) b.set_ylabel('pior quadro (ms)') b.text(t[-1], ideal + 3, 'quadro ideal: %.1f ms' % ideal, ha='right', va='bottom', color=muted, fontsize=9) c.plot(t, draws, color='#059669', lw=1.4) c.set_ylabel('draws/quadro') c.set_xlabel('minutos de sessão') steady = [f for f in fps[6:]] or fps target = cap or 30.0 at30 = sum(1 for f in steady if f >= target - 0.5) / len(steady) * 100 at60 = sum(1 for f in steady if f >= 59.5) / len(steady) * 100 p99 = statistics.median(p[2] for p in pacing) if pacing else float('nan') game = sys.argv[3] if len(sys.argv) > 3 else 'The Last Guardian' fig.suptitle('%s no PC — sessão de %.0f min (Linux, Vulkan, RTX 2060 6 GB)' % (game, t[-1]), x=0.07, ha='left', color=ink, fontsize=13, fontweight='bold') if cap: summary = '%.0f%% das janelas de 5 s a %.0f FPS · p99 do quadro %.1f ms · %d a %d draws por quadro' % ( at30, cap, p99, min(draws), max(draws)) else: summary = ('sem trava de FPS · mediana %.0f FPS · %.0f%% das janelas a 30+ FPS, %.0f%% a 60 · ' '%d a %d draws por quadro') % (statistics.median(steady), at30, at60, min(draws), max(draws)) fig.text(0.07, 0.915, summary, color=muted, fontsize=10) fig.tight_layout(rect=(0, 0, 1, 0.92)) fig.savefig(sys.argv[2], dpi=110) print(f'{len(stats)} amostras, mediana {statistics.median(steady):.1f} FPS, {at30:.0f}% a {target:.0f}+ FPS, ' f'p99 {p99:.2f} ms, draws {min(draws):.0f}-{max(draws):.0f}')