using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; using Luneraria.Core; using UnityEngine; namespace Luneraria.View { /// /// Frame-time benchmark of a player build (U2D). Active only with the command-line argument /// "-luneBenchmark <out.json>": runs the match of seed 9 from round 21 (the golden fixture /// high_wave_21, the most units) at normal speed, VSync off, and measures two windows once the /// armies meet: the whole map (20 s) and a close view of the Mid lane (10 s). Then it quits. /// [DefaultExecutionOrder(-100)] public sealed class LuneBenchmark : MonoBehaviour { public LuneMatchDriver Driver; public LuneMatchView View; public LuneCameraRig Rig; sealed class Window { public string Label; public float Seconds; public float Zoom; public Vec2 Focus; public readonly List FrameMs = new List(); public double Simulation; public double Views; public int Units; public int GcStart; public int GcEnd; public int SlowFrames; public int SlowWithStep; public readonly List MainThreadMs = new List(); public readonly List RenderThreadMs = new List(); public readonly List GpuMs = new List(); public readonly List PresentWaitMs = new List(); } readonly FrameTiming[] _timings = new FrameTiming[1]; string _out; readonly List _windows = new List(); int _index = -1; float _elapsed; void Awake() { string[] args = Environment.GetCommandLineArgs(); int at = Array.IndexOf(args, "-luneBenchmark"); if (at < 0 || at + 1 >= args.Length) { enabled = false; return; } _out = args[at + 1]; Running = true; // The benchmark runs unattended; it must not pause when its window loses focus. Application.runInBackground = true; QualitySettings.vSyncCount = 0; Application.targetFrameRate = -1; _windows.Add(new Window { Label = "overview (zoom 64)", Seconds = 20f, Zoom = 64f, Focus = new Vec2(0, 0) }); _windows.Add(new Window { Label = "mid lane close (zoom 22)", Seconds = 10f, Zoom = 22f, Focus = new Vec2(0, 0) }); } /// True in a benchmark run: the game controller keeps the bot match (no human seat). public static bool Running { get; private set; } void Start() { Driver.HumanSlot = -1; Driver.Seed = 9; Driver.StartRound = 21; Driver.ExtraGold = 4000; Driver.Speed = 1f; Driver.Restart(); Rig.Interactive = false; Rig.Overview(true); } void Update() { LuneMatch match = Driver.Runner.Match; if (_index < 0) { if (Driver.Runner.Current == LuneMatchRunner.Stage.Combat && match.Combat.Elapsed >= 10.0) Begin(0); return; } if (_index >= _windows.Count) return; Window window = _windows[_index]; double ms = Time.unscaledDeltaTime * 1000.0; window.FrameMs.Add(ms); // Where the time goes (needs "Frame Timing Stats" in the player settings). FrameTimingManager.CaptureFrameTimings(); if (FrameTimingManager.GetLatestTimings(1, _timings) > 0) { window.MainThreadMs.Add(_timings[0].cpuMainThreadFrameTime); window.RenderThreadMs.Add(_timings[0].cpuRenderThreadFrameTime); window.GpuMs.Add(_timings[0].gpuFrameTime); window.PresentWaitMs.Add(_timings[0].cpuMainThreadPresentWaitTime); } // Frames above 1/60 s, and how many of them also ran a simulation step. if (ms > 1000.0 / 60.0) { window.SlowFrames++; if (Driver.Runner.StepsThisFrame > 0) window.SlowWithStep++; } window.Simulation += Driver.LastAdvanceMs; window.Views += View.LastSyncMs; if (match.Combat != null && window.FrameMs.Count % 30 == 1) window.Units = Math.Max(window.Units, CountAlive(match.Combat)); _elapsed += Time.unscaledDeltaTime; if (_elapsed >= window.Seconds) { window.GcEnd = GC.CollectionCount(0); if (_index + 1 < _windows.Count) Begin(_index + 1); else Finish(); } } static string Describe(List values) { if (values.Count == 0) return "n/a"; double[] sorted = values.OrderBy(v => v).ToArray(); return string.Format(CultureInfo.InvariantCulture, "avg {0:0.00} / median {1:0.00} / p95 {2:0.00} ms", sorted.Average(), sorted[sorted.Length / 2], sorted[(int)(sorted.Length * .95)]); } static int CountAlive(LuneCombat combat) { int count = 0; foreach (LuneUnit unit in combat.Units) if (unit.Alive && unit.IsMobile()) count++; return count; } void Begin(int index) { _index = index; _elapsed = 0f; Window window = _windows[index]; window.GcStart = GC.CollectionCount(0); Rig.LookAt(window.Focus, window.Zoom, true); } void Finish() { _index = _windows.Count; var text = new System.Text.StringBuilder("{\n"); text.AppendFormat(" \"build\": \"{0} player, {1}, {2}\",\n", Application.platform, Application.unityVersion, Debug.isDebugBuild ? "development" : "release"); text.AppendFormat(" \"device\": \"{0}; {1}; {2}\",\n", SystemInfo.deviceModel, SystemInfo.processorType, SystemInfo.graphicsDeviceName); text.AppendFormat(" \"graphics_api\": \"{0}\",\n \"resolution\": \"{1}x{2}\",\n \"vsync\": {3},\n", SystemInfo.graphicsDeviceType, Screen.width, Screen.height, QualitySettings.vSyncCount); text.Append(" \"match\": \"seed 9 from round 21 (fixture high_wave_21), speed 1x, measured after 10 s of combat\",\n \"windows\": [\n"); for (int i = 0; i < _windows.Count; i++) { Window window = _windows[i]; double[] sorted = window.FrameMs.OrderBy(v => v).ToArray(); double total = window.FrameMs.Sum(); text.AppendFormat(CultureInfo.InvariantCulture, " {{\"view\": \"{0}\", \"frames\": {1}, \"fps\": {2:0.0}, \"frame_ms_avg\": {3:0.00}, \"frame_ms_median\": {4:0.00}, \"frame_ms_p95\": {5:0.00}, \"frame_ms_p99\": {6:0.00}, \"frame_ms_max\": {7:0.00}, \"frames_over_16_7_ms\": {8}, \"of_them_with_simulation_step\": {9}, \"gc_collections\": {10}, \"simulation_ms_per_frame\": {11:0.000}, \"views_ms_per_frame\": {12:0.000}, \"units_alive_max\": {13}}}{14}\n", window.Label, sorted.Length, sorted.Length / (total / 1000.0), total / sorted.Length, sorted[sorted.Length / 2], sorted[(int)(sorted.Length * .95)], sorted[(int)(sorted.Length * .99)], sorted[sorted.Length - 1], window.SlowFrames, window.SlowWithStep, window.GcEnd - window.GcStart, window.Simulation / sorted.Length, window.Views / sorted.Length, window.Units, i < _windows.Count - 1 ? "," : ""); text.AppendFormat(CultureInfo.InvariantCulture, " // timings {0}: main thread {1}, render thread {2}, gpu {3}, present wait {4}\n", window.Label, Describe(window.MainThreadMs), Describe(window.RenderThreadMs), Describe(window.GpuMs), Describe(window.PresentWaitMs)); } text.Append(" ]\n}\n"); Directory.CreateDirectory(Path.GetDirectoryName(_out)); File.WriteAllText(_out, text.ToString()); Application.Quit(); } } }