using System.Diagnostics; using UnityEditor; using UnityEngine; using UnityEngine.Profiling; using Ashwild.Building; using Debug = UnityEngine.Debug; namespace Ashwild.EditorTools { /// /// A load test for the decision to spawn every build as a NetworkObject: places a grid of structures /// and reports how long it took and what it cost in memory, so the "can FishNet carry a whole base" /// question can be answered with numbers instead of estimates. /// /// Deliberately single-instance. Measuring a second client's join bandwidth would mean two peers, and /// the session is wired to FishySteamworks — that costs far more setup than the risk it covers. Join /// traffic is a one-shot cost of roughly a spawn packet per object anyway; what actually matters day /// to day is the steady state, and that is exactly what this measures. Watch the profiler's frame time /// after the spawn, not just the numbers logged here. /// /// Caveat when reading the elapsed time: this places everything in a single frame, which no player /// ever does. Treat it as a worst case for the spawn burst, not as a gameplay measurement. /// public static class BuildStressTest { #region Constants private const float Spacing = 4f; private const float GroundHeight = 0f; #endregion #region Menu /// /// Places 250 structures — a realistic co-op base. /// [MenuItem("Tools/Ashwild/Stress Test/Spawn 250 Builds")] public static void Spawn250() => SpawnGrid(250); /// /// Places 1000 structures — an ambitious long-save base, the level the NetworkObject decision was /// judged against. /// [MenuItem("Tools/Ashwild/Stress Test/Spawn 1000 Builds")] public static void Spawn1000() => SpawnGrid(1000); #endregion #region Internal Helpers /// /// Commits builds of the first usable buildable on a grid around the /// origin, then logs the elapsed time and the memory delta. Requires play mode with a live session, /// since committing goes through the registry's server RPC exactly as a real placement would. /// private static void SpawnGrid(int count) { if (!Application.isPlaying) { Debug.LogError("[BuildStressTest] Enter play mode and host a session first — builds are spawned through the server."); return; } if (BuildRegistry.Instance == null) { Debug.LogError("[BuildStressTest] No BuildRegistry in the session — is the scene's BuildRegistry present and the session started?"); return; } if (!TryResolveBuildable(out ushort id, out string label)) return; int side = Mathf.CeilToInt(Mathf.Sqrt(count)); long memoryBefore = Profiler.GetTotalAllocatedMemoryLong(); Stopwatch watch = Stopwatch.StartNew(); for (int i = 0; i < count; i++) { Vector3 position = new Vector3((i % side) * Spacing, GroundHeight, (i / side) * Spacing); BuildRegistry.Instance.RequestBuild(id, position, Quaternion.identity); } watch.Stop(); long memoryDelta = Profiler.GetTotalAllocatedMemoryLong() - memoryBefore; Debug.Log($"[BuildStressTest] Requested {count}× '{label}' in {watch.ElapsedMilliseconds} ms " + $"({memoryDelta / 1024f / 1024f:F1} MB allocated this frame). " + "Spawns complete over the next frames — check the profiler's steady-state frame time now."); } /// /// Picks the first buildable that can actually be spawned, so the test never fails halfway on a /// half-authored asset. Reports clearly when the database is empty or unbuilt. /// private static bool TryResolveBuildable(out ushort id, out string label) { id = 0; label = string.Empty; BuildableDatabase database = BuildableDatabase.Instance; if (database == null || database.Buildables == null || database.Buildables.Length == 0) { Debug.LogError("[BuildStressTest] BuildableDatabase is empty — run Tools ▸ Ashwild ▸ Rebuild Buildable Database."); return false; } foreach (BuildableData buildable in database.Buildables) { if (buildable == null || buildable.BuiltPrefab == null) continue; id = database.GetId(buildable); label = buildable.name; return true; } Debug.LogError("[BuildStressTest] No buildable with a BuiltPrefab found."); return false; } #endregion } }