| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177 |
- #nullable enable
- using System;
- using System.Linq;
- using System.Threading.Tasks;
- using BenchmarkDotNet.Attributes;
- using Content.IntegrationTests;
- using Content.IntegrationTests.Pair;
- using Content.Server.Mind;
- using Content.Server.Warps;
- using Robust.Shared;
- using Robust.Shared.Analyzers;
- using Robust.Shared.EntitySerialization;
- using Robust.Shared.EntitySerialization.Systems;
- using Robust.Shared.GameObjects;
- using Robust.Shared.Map;
- using Robust.Shared.Player;
- using Robust.Shared.Random;
- using Robust.Shared.Utility;
- 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 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();
- _pair = PoolManager.GetServerClient().GetAwaiter().GetResult();
- _entMan = _pair.Server.ResolveDependency<IEntityManager>();
- _pair.Server.CfgMan.SetCVar(CVars.NetPVS, true);
- _pair.Server.CfgMan.SetCVar(CVars.ThreadParallelCount, 0);
- _pair.Server.CfgMan.SetCVar(CVars.NetPvsAsync, false);
- _sys = _entMan.System<SharedTransformSystem>();
- SetupAsync().Wait();
- }
- private async Task SetupAsync()
- {
- // Spawn the map
- _pair.Server.ResolveDependency<IRobustRandom>().SetSeed(42);
- await _pair.Server.WaitPost(() =>
- {
- var path = new ResPath(Map);
- var opts = DeserializationOptions.Default with {InitializeMaps = true};
- if (!_entMan.System<MapLoaderSystem>().TryLoadMap(path, out _, out _, opts))
- throw new Exception("Map load failed");
- });
- // Get list of ghost warp positions
- _spawns = _entMan.AllComponentsList<WarpPointComponent>()
- .OrderBy(x => x.Component.Location)
- .Select(x => _entMan.GetComponent<TransformComponent>(x.Uid).Coordinates)
- .ToArray();
- Array.Resize(ref _players, PlayerCount);
- // Spawn "Players"
- _players = await _pair.Server.AddDummySessions(PlayerCount);
- await _pair.Server.WaitPost(() =>
- {
- var mind = _pair.Server.System<MindSystem>();
- 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");
- mind.ControlMob(_players[i].UserId, uid);
- }
- });
- // 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<TransformComponent>(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<TransformComponent>(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);
- }
- /// <summary>
- /// 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
- /// </summary>
- [Benchmark]
- public void StaticTick()
- {
- _pair.Server.PvsTick(_players);
- }
- /// <summary>
- /// Basic benchmark for PVS in a situation where players are teleporting all over the place. This isn't very
- /// realistic, but unlike <see cref="StaticTick"/> this will actually also measure the speed of processing dirty
- /// chunks and sending PVS leave messages.
- /// </summary>
- [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);
- }
- }
|