diff --git a/Content.Benchmarks/MapLoadBenchmark.cs b/Content.Benchmarks/MapLoadBenchmark.cs index 5d94ef85cb..7caa995836 100644 --- a/Content.Benchmarks/MapLoadBenchmark.cs +++ b/Content.Benchmarks/MapLoadBenchmark.cs @@ -46,7 +46,7 @@ public class MapLoadBenchmark PoolManager.Shutdown(); } - public static IEnumerable MapsSource { get; set; } + public static readonly string[] MapsSource = { "Empty", "Box", "Aspid", "Bagel", "Dev", "CentComm", "Atlas", "Core", "TestTeg", "Saltern", "Packed", "Omega", "Cluster", "Gemini", "Reach", "Origin", "Meta", "Marathon", "Europa", "MeteorArena", "Fland", "Barratry" }; [ParamsSource(nameof(MapsSource))] public string Map; diff --git a/Content.Benchmarks/Program.cs b/Content.Benchmarks/Program.cs index 65b5abaf73..0beb0a613d 100644 --- a/Content.Benchmarks/Program.cs +++ b/Content.Benchmarks/Program.cs @@ -23,13 +23,6 @@ namespace Content.Benchmarks public static async Task MainAsync(string[] args) { - PoolManager.Startup(typeof(Program).Assembly); - var pair = await PoolManager.GetServerClient(); - var gameMaps = pair.Server.ResolveDependency().EnumeratePrototypes().ToList(); - MapLoadBenchmark.MapsSource = gameMaps.Select(x => x.ID); - await pair.CleanReturnAsync(); - PoolManager.Shutdown(); - #if DEBUG Console.ForegroundColor = ConsoleColor.Red; Console.WriteLine("\nWARNING: YOU ARE RUNNING A DEBUG BUILD, USE A RELEASE BUILD FOR AN ACCURATE BENCHMARK"); diff --git a/Content.Benchmarks/PvsBenchmark.cs b/Content.Benchmarks/PvsBenchmark.cs new file mode 100644 index 0000000000..c7f22bdb0c --- /dev/null +++ b/Content.Benchmarks/PvsBenchmark.cs @@ -0,0 +1,187 @@ +#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; } + } +}