#nullable enable using System; using System.Collections.Generic; using System.Linq; using BenchmarkDotNet.Attributes; using Content.IntegrationTests; using Content.IntegrationTests.Pair; using Content.Server.Warps; using Robust.Server.GameObjects; using Robust.Shared; using Robust.Shared.Analyzers; using Robust.Shared.Enums; using Robust.Shared.GameObjects; using Robust.Shared.GameStates; using Robust.Shared.Map; using Robust.Shared.Network; using Robust.Shared.Player; using Robust.Shared.Random; namespace Content.Benchmarks; // This benchmark probably benefits from some accidental cache locality. I,e. the order in which entities in a pvs // chunk are sent to players matches the order in which the entities were spawned. // // in a real mid-late game round, this is probably no longer the case. // One way to somewhat offset this is to update the NetEntity assignment to assign random (but still unique) NetEntity uids to entities. // This makes the benchmark run noticeably slower. [Virtual] public class PvsBenchmark { public const string Map = "Maps/box.yml"; [Params(1, 8, 80)] public int PlayerCount { get; set; } private TestPair _pair = default!; private IEntityManager _entMan = default!; private MapId _mapId = new(10); private ICommonSession[] _players = default!; private EntityCoordinates[] _spawns = default!; public int _cycleOffset = 0; private SharedTransformSystem _sys = default!; private EntityCoordinates[] _locations = default!; [GlobalSetup] public void Setup() { #if !DEBUG ProgramShared.PathOffset = "../../../../"; #endif PoolManager.Startup(null); _pair = PoolManager.GetServerClient().GetAwaiter().GetResult(); _entMan = _pair.Server.ResolveDependency(); _pair.Server.CfgMan.SetCVar(CVars.NetPVS, true); _pair.Server.CfgMan.SetCVar(CVars.ThreadParallelCount, 0); _pair.Server.CfgMan.SetCVar(CVars.NetPvsAsync, false); _sys = _entMan.System(); // Spawn the map _pair.Server.ResolveDependency().SetSeed(42); _pair.Server.WaitPost(() => { var success = _entMan.System().TryLoad(_mapId, Map, out _); if (!success) throw new Exception("Map load failed"); _pair.Server.MapMan.DoMapInitialize(_mapId); }).Wait(); // Get list of ghost warp positions _spawns = _entMan.AllComponentsList() .OrderBy(x => x.Component.Location) .Select(x => _entMan.GetComponent(x.Uid).Coordinates) .ToArray(); Array.Resize(ref _players, PlayerCount); // Spawn "Players". _pair.Server.WaitPost(() => { for (var i = 0; i < PlayerCount; i++) { var pos = _spawns[i % _spawns.Length]; var uid =_entMan.SpawnEntity("MobHuman", pos); _pair.Server.ConsoleHost.ExecuteCommand($"setoutfit {_entMan.GetNetEntity(uid)} CaptainGear"); _players[i] = new DummySession{AttachedEntity = uid}; } }).Wait(); // Repeatedly move players around so that they "explore" the map and see lots of entities. // This will populate their PVS data with out-of-view entities. var rng = new Random(42); ShufflePlayers(rng, 100); _pair.Server.PvsTick(_players); _pair.Server.PvsTick(_players); var ents = _players.Select(x => x.AttachedEntity!.Value).ToArray(); _locations = ents.Select(x => _entMan.GetComponent(x).Coordinates).ToArray(); } private void ShufflePlayers(Random rng, int count) { while (count > 0) { ShufflePlayers(rng); count--; } } private void ShufflePlayers(Random rng) { _pair.Server.PvsTick(_players); var ents = _players.Select(x => x.AttachedEntity!.Value).ToArray(); var locations = ents.Select(x => _entMan.GetComponent(x).Coordinates).ToArray(); // Shuffle locations var n = locations.Length; while (n > 1) { n -= 1; var k = rng.Next(n + 1); (locations[k], locations[n]) = (locations[n], locations[k]); } _pair.Server.WaitPost(() => { for (var i = 0; i < PlayerCount; i++) { _sys.SetCoordinates(ents[i], locations[i]); } }).Wait(); _pair.Server.PvsTick(_players); } /// /// Basic benchmark for PVS in a static situation where nothing moves or gets dirtied.. /// This effectively provides a lower bound on "real" pvs tick time, as it is missing: /// - PVS chunks getting dirtied and needing to be rebuilt /// - Fetching component states for dirty components /// - Compressing & sending network messages /// - Sending PVS leave messages /// [Benchmark] public void StaticTick() { _pair.Server.PvsTick(_players); } /// /// Basic benchmark for PVS in a situation where players are teleporting all over the place. This isn't very /// realistic, but unlike this will actually also measure the speed of processing dirty /// chunks and sending PVS leave messages. /// [Benchmark] public void CycleTick() { _cycleOffset = (_cycleOffset + 1) % _players.Length; _pair.Server.WaitPost(() => { for (var i = 0; i < PlayerCount; i++) { _sys.SetCoordinates(_players[i].AttachedEntity!.Value, _locations[(i + _cycleOffset) % _players.Length]); } }).Wait(); _pair.Server.PvsTick(_players); } private sealed class DummySession : ICommonSession { public SessionStatus Status => SessionStatus.InGame; public EntityUid? AttachedEntity {get; set; } public NetUserId UserId => default; public string Name => string.Empty; public short Ping => default; public INetChannel Channel { get; set; } = default!; public LoginType AuthType => default; public HashSet ViewSubscriptions { get; } = new(); public DateTime ConnectedTime { get; set; } public SessionState State => default!; public SessionData Data => default!; public bool ClientSide { get; set; } } }