using System;
using System.Collections.Generic;
using System.Text;
using UnityEngine;
namespace MSCO
{
///
/// Laufzeit-Statistik: wie viele ms pro Bild jeder MSCO-Teil braucht, FPS und Garbage-Collections. Konsole (F7):
/// "perf" bzw. "perf reset". Mit der Startoption -msco-perf wird alle 30 s eine Zeile in MSCO\MSCO_Loader.txt
/// geschrieben (zum Messen ohne Konsole).
///
public static class PerfStats
{
static readonly Dictionary totals = new Dictionary();
/// Aufrufe der Harmony-Haken (Fsm.SwitchState, Fsm.Event, cInput) - zum Abschätzen ihrer Kosten.
public static int SwitchStateCalls, EventCalls, InputCalls;
static int frames, gcStart = -1;
static double frameSeconds, worstFrame;
static float started = -1f;
static int level = -1;
static int reports;
static readonly bool autoLog = Array.IndexOf(Environment.GetCommandLineArgs(), "-msco-perf") >= 0;
public static void Add(string part, double ms)
{
double t;
totals.TryGetValue(part, out t);
totals[part] = t + ms;
}
/// Einmal pro Bild (Update).
public static void Frame(float dt)
{
// Neue Szene (Menü -> Spiel): neu anfangen, sonst zählt das Laden mit
if (started < 0f || Application.loadedLevel != level) { level = Application.loadedLevel; reports = 0; Reset(); }
frames++;
frameSeconds += dt;
if (dt > worstFrame) worstFrame = dt;
// Mit -msco-perf alle 30 s, sonst im Spiel nach 2 Minuten und dann alle 5 Minuten ein Bericht in MSCO_Loader.txt -
// so lässt sich bei "wenig FPS" nachsehen, ob MSCO, eine Mod oder das Spiel selbst die Zeit braucht
float every = autoLog ? 30f : reports == 0 ? 120f : 300f;
if ((autoLog || MenuHelper.InGame) && Time.realtimeSinceStartup - started >= every)
{
Bootstrap.Log(Report());
reports++;
Reset();
}
}
/// Beim Beenden: was seit dem letzten Bericht gemessen wurde (mindestens 20 s im Spiel).
public static void FinalReport()
{
if (frames > 0 && MenuHelper.InGame && Time.realtimeSinceStartup - started >= 20f) Bootstrap.Log(Report());
}
public static void Reset()
{
totals.Clear();
frames = 0;
frameSeconds = 0;
worstFrame = 0;
started = Time.realtimeSinceStartup;
gcStart = GC.CollectionCount(0);
SwitchStateCalls = EventCalls = InputCalls = 0;
}
public static string Report()
{
if (frames == 0) return "PERF: no frames measured yet";
double seconds = Math.Max(0.001, Time.realtimeSinceStartup - started);
double msco = 0;
foreach (double v in totals.Values) msco += v;
var parts = new List>(totals);
parts.Sort((a, b) => b.Value.CompareTo(a.Value));
var sb = new StringBuilder();
sb.Append("PERF ").Append(seconds.ToString("0")).Append(" s (").Append(MenuHelper.Scene).Append("): ")
.Append((frames / frameSeconds).ToString("0")).Append(" fps, ")
.Append((frameSeconds * 1000.0 / frames).ToString("0.0")).Append(" ms/frame (worst ")
.Append((worstFrame * 1000.0).ToString("0")).Append(" ms), MSCO ")
.Append((msco / frames).ToString("0.00")).Append(" ms/frame, GC ")
.Append(((GC.CollectionCount(0) - gcStart) * 60.0 / seconds).ToString("0.0")).Append("/min")
.Append(", hooks/frame: state ").Append((SwitchStateCalls / (double)frames).ToString("0"))
.Append(" event ").Append((EventCalls / (double)frames).ToString("0"))
.Append(" input ").Append((InputCalls / (double)frames).ToString("0"));
for (int i = 0; i < parts.Count && i < 8; i++)
sb.Append(i == 0 ? " | " : ", ").Append(parts[i].Key).Append(' ').Append((parts[i].Value / frames).ToString("0.000"));
return sb.ToString();
}
}
}