using System; using System.Linq; using System.Numerics; using System.Threading.Tasks; using BenchmarkDotNet.Attributes; using Robust.Server.Containers; using Robust.Server.GameStates; using Robust.Shared; using Robust.Shared.Analyzers; using Robust.Shared.Configuration; using Robust.Shared.Containers; using Robust.Shared.GameObjects; using Robust.Shared.Map; using Robust.Shared.Maths; using Robust.Shared.Network; using Robust.Shared.Player; using Robust.UnitTesting; namespace Robust.Benchmarks.Transform; /// /// This benchmark tests various transform/move related functions with an entity that has many children. /// [Virtual, MemoryDiagnoser] public class RecursiveMoveBenchmark : RobustIntegrationTest { private IEntityManager _entMan = default!; private SharedTransformSystem _transform = default!; private ContainerSystem _container = default!; private PvsSystem _pvs = default!; private EntityCoordinates _mapCoords; private EntityCoordinates _gridCoords; private EntityCoordinates _gridCoords2; private EntityUid _ent; private EntityUid _child; private TransformComponent _childXform = default!; private EntityQuery _query; private ICommonSession[] _players = default!; private PvsSession _session = default!; [GlobalSetup] public void GlobalSetup() { ProgramShared.PathOffset = "../../../../"; var server = StartServer(new() {Pool = false}); var client = StartClient(new() {Pool = false}); Task.WhenAll(client.WaitIdleAsync(), server.WaitIdleAsync()).Wait(); _entMan = server.ResolveDependency(); var confMan = server.ResolveDependency(); var sPlayerMan = server.ResolveDependency(); _transform = _entMan.System(); _container = _entMan.System(); _pvs = _entMan.System(); _query = _entMan.GetEntityQuery(); var mapSys = _entMan.System(); var netMan = client.ResolveDependency(); client.SetConnectTarget(server); client.Post(() => netMan.ClientConnect(null!, 0, null!)); server.Post(() => confMan.SetCVar(CVars.NetPVS, true)); for (int i = 0; i < 10; i++) { server.WaitRunTicks(1).Wait(); client.WaitRunTicks(1).Wait(); } // Ensure client & server ticks are synced. // Client runs 1 tick ahead { var sTick = (int)server.Timing.CurTick.Value; var cTick = (int)client.Timing.CurTick.Value; var delta = cTick - sTick; if (delta > 1) server.WaitRunTicks(delta - 1).Wait(); else if (delta < 1) client.WaitRunTicks(1 - delta).Wait(); sTick = (int)server.Timing.CurTick.Value; cTick = (int)client.Timing.CurTick.Value; delta = cTick - sTick; if (delta != 1) throw new Exception("Failed setup"); } // Set up map and spawn player server.WaitPost(() => { var map = server.ResolveDependency().CreateMap(out var mapId); var gridComp = mapSys.CreateGridEntity(mapId); var grid = gridComp.Owner; mapSys.SetTile(grid, gridComp, Vector2i.Zero, new Tile(1)); _gridCoords = new EntityCoordinates(grid, .5f, .5f); _gridCoords2 = new EntityCoordinates(grid, .5f, .6f); _mapCoords = new EntityCoordinates(map, 100, 100); var playerUid = _entMan.SpawnEntity(null, _mapCoords); // Attach player. var session = sPlayerMan.Sessions.First(); server.PlayerMan.SetAttachedEntity(session, playerUid); sPlayerMan.JoinGame(session); // Next, we will spawn our test entity. This entity will have a complex transform/container hierarchy. // This is intended to be representative of a typical SS14 player entity, with organs. clothing, and a full backpack. _ent = _entMan.Spawn(); // Quick check that SetCoordinates actually changes the parent as expected // I.e., ensure that grid-traversal code doesn't just dump the entity on the map. _transform.SetCoordinates(_ent, _gridCoords); if (_query.GetComponent(_ent).ParentUid != _gridCoords.EntityId) throw new Exception("Grid traversal error."); _transform.SetCoordinates(_ent, _mapCoords); if (_query.GetComponent(_ent).ParentUid != _mapCoords.EntityId) throw new Exception("Grid traversal error."); // Add 5 direct children in slots to represent clothing. for (var i = 0; i < 5; i++) { var id = $"inventory{i}"; _container.EnsureContainer(_ent, id); if (!_entMan.TrySpawnInContainer(null, _ent, id, out _)) throw new Exception($"Failed to setup entity"); } // body parts _container.EnsureContainer(_ent, "body"); for (var i = 0; i < 5; i++) { // Simple organ if (!_entMan.TrySpawnInContainer(null, _ent, "body", out _)) throw new Exception($"Failed to setup entity"); // body part that has another body part / limb if (!_entMan.TrySpawnInContainer(null, _ent, "body", out var limb)) throw new Exception($"Failed to setup entity"); _container.EnsureContainer(limb.Value, "limb"); if (!_entMan.TrySpawnInContainer(null, limb.Value, "limb", out _)) throw new Exception($"Failed to setup entity"); } // Backpack _container.EnsureContainer(_ent, "inventory-backpack"); if (!_entMan.TrySpawnInContainer(null, _ent, "inventory-backpack", out var backpack)) throw new Exception($"Failed to setup entity"); // Misc backpack contents. var backpackStorage = _container.EnsureContainer(backpack.Value, "storage"); for (var i = 0; i < 10; i++) { if (!_entMan.TrySpawnInContainer(null, backpack.Value, "storage", out _)) throw new Exception($"Failed to setup entity"); } // Emergency box inside of the backpack var box = backpackStorage.ContainedEntities.First(); var boxContainer = _container.EnsureContainer(box, "storage"); for (var i = 0; i < 10; i++) { if (!_entMan.TrySpawnInContainer(null, box, "storage", out _)) throw new Exception($"Failed to setup entity"); } // Deepest child. _child = boxContainer.ContainedEntities.First(); _childXform = _query.GetComponent(_child); _players = new[] {session}; _session = _pvs.PlayerData[session]; }).Wait(); for (int i = 0; i < 10; i++) { server.WaitRunTicks(1).Wait(); client.WaitRunTicks(1).Wait(); } PvsTick(); PvsTick(); } private void PvsTick() { _session.ClearState(); _pvs.CacheSessionData(_players); _pvs.GetVisibleChunks(); _pvs.ProcessVisibleChunksSequential(); } /// /// This implicitly measures move events, including PVS and entity lookups. Though given that most of the entities /// are in containers, this will bias the entity lookup aspect. /// [Benchmark] public void MoveEntity() { _transform.SetCoordinates(_ent, _gridCoords); _transform.SetCoordinates(_ent, _mapCoords); } [Benchmark] public void MoveEntityASmidge() { _transform.SetCoordinates(_ent, _gridCoords); _transform.SetCoordinates(_ent, _gridCoords2); } /// /// Like , but also processes queued PVS chunk updates. /// [Benchmark] public void MoveAndUpdateChunks() { _transform.SetCoordinates(_ent, _gridCoords); PvsTick(); _transform.SetCoordinates(_ent, _mapCoords); PvsTick(); } [Benchmark] public void MoveASmidgeAndUpdateChunk() { _transform.SetCoordinates(_ent, _gridCoords); PvsTick(); _transform.SetCoordinates(_ent, _gridCoords2); PvsTick(); } [Benchmark] public Vector2 GetWorldPos() { return _transform.GetWorldPosition(_childXform); } [Benchmark] public EntityUid GetRootUid() { var xform = _childXform; while (xform.ParentUid.IsValid()) { xform = _query.GetComponent(xform.ParentUid); } return xform.ParentUid; } }