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. /// Since U3 the scene carries the HUD (watching seat 1 of the bot match); its own scripts' /// cost per frame comes through UiMs (layout and drawing of the panel count in the threads). /// U5.5: "-luneBenchmarkRound <n>" starts from another round, and "-luneBenchmarkArmy /// <unit>" is a figure stress test: every seat whose Bund owns the unit fields only that /// unit up to the supply cap (a fixture like those of the HUD view tests: public match state /// and planning commands; the other seats keep their bot plans). The report lists the moving /// units by definition at the start of the first window. H1: a hero id ("held_sylvara") gives /// every seat without a committed hero that hero (start from round 2, before the bots recruit). /// [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 double Ui; 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; int _round = 21; string _army; int _fielded; string _census = ""; 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]; int roundAt = Array.IndexOf(args, "-luneBenchmarkRound"); if (roundAt >= 0 && roundAt + 1 < args.Length && int.TryParse(args[roundAt + 1], NumberStyles.Integer, CultureInfo.InvariantCulture, out int round) && round > 0) _round = round; int armyAt = Array.IndexOf(args, "-luneBenchmarkArmy"); if (armyAt >= 0 && armyAt + 1 < args.Length) _army = args[armyAt + 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; } /// Main-thread cost of the HUD scripts in the last frame (set by the game controller, U3). public static Func UiMs; void Start() { Driver.HumanSlot = -1; Driver.Seed = 9; Driver.StartRound = _round; Driver.ExtraGold = 4000; Driver.Speed = 1f; Driver.Restart(); if (!string.IsNullOrEmpty(_army)) _fielded = FieldArmy(Driver.Runner.Match, _army); 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; window.Ui += UiMs != null ? UiMs() : 0.0; 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; } /// /// The figure stress fixture: seats whose Bund owns the unit replace their planned army by /// that unit, bought with planning commands up to the supply cap. Returns how many. /// static int FieldArmy(LuneMatch match, string typeId) { int total = 0; // H1: a hero id gives every seat that hero (on top of its planned army). if (ContentData.Heroes.ContainsKey(typeId)) { foreach (Player player in match.Players) { Draft draft = player.Draft; if (draft.Hero != null && draft.Hero.Fresh) match.Submit(player.Slot, Command.HeroCancel()); int gold = draft.Gold; draft.Gold = 100000; if (draft.Hero == null && match.Submit(player.Slot, Command.HeroBuy(typeId)).Ok) total++; draft.Gold = gold; } return total; } foreach (Player player in match.Players) { Draft draft = player.Draft; var planned = new List(draft.Roster); int gold = draft.Gold; draft.Roster.Clear(); draft.Gold = 100000; int bought = 0; while (bought < 40 && match.Submit(player.Slot, Command.Buy(typeId, player.Lane)).Ok) bought++; // An upgraded troop (g02 Schlackenklaue) is bought as its base and upgraded. string baseId = bought == 0 ? ContentData.Units.Keys.FirstOrDefault(id => DataCatalog.UpgradeOf(id) == typeId) : null; if (baseId != null) { while (bought < 40 && match.Submit(player.Slot, Command.Buy(baseId, player.Lane)).Ok) bought++; foreach (RosterEntry entry in draft.Roster.ToList()) if (!match.Submit(player.Slot, Command.Upgrade(entry.Uid)).Ok) bought--; } if (bought == 0) draft.Roster.AddRange(planned); draft.Gold = gold; total += bought; } return total; } /// Moving units alive by definition, most first ("g01 24, w01 20, ..."). static string Census(LuneCombat combat) { var counts = new Dictionary(); foreach (LuneUnit unit in combat.Units) { if (!unit.Alive || !unit.IsMobile()) continue; counts.TryGetValue(unit.Def.Id, out int count); counts[unit.Def.Id] = count + 1; } return string.Join(", ", counts.OrderByDescending(p => p.Value).ThenBy(p => p.Key, StringComparer.Ordinal).Select(p => p.Key + " " + p.Value)); } void Begin(int index) { _index = index; _elapsed = 0f; if (index == 0 && Driver.Runner.Match.Combat != null) _census = Census(Driver.Runner.Match.Combat); 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); string army = string.IsNullOrEmpty(_army) ? "" : string.Format(CultureInfo.InvariantCulture, ", army {0} x{1}", _army, _fielded); text.AppendFormat(CultureInfo.InvariantCulture, " \"match\": \"seed 9 from round {0}{1}{2}, speed 1x, measured after 10 s of combat\",\n", _round, _round == 21 && army.Length == 0 ? " (fixture high_wave_21)" : "", army); text.AppendFormat(" // moving units at the start of the first window: {0}\n", _census); text.Append(" \"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}, \"ui_ms_per_frame\": {15: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 ? "," : "", window.Ui / sorted.Length); 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(); } } }