]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Add PVS benchmark (#23166)
authorLeon Friedrich <60421075+ElectroJr@users.noreply.github.com>
Thu, 4 Jan 2024 00:58:38 +0000 (19:58 -0500)
committerGitHub <noreply@github.com>
Thu, 4 Jan 2024 00:58:38 +0000 (11:58 +1100)
* Add PVS benchmark

* poke tests

* Shuffle players around

* Add caveat

* Add CycleTick() benchmark

* Make async false

* Oops

Content.Benchmarks/MapLoadBenchmark.cs
Content.Benchmarks/Program.cs
Content.Benchmarks/PvsBenchmark.cs [new file with mode: 0644]

index 5d94ef85cbd5c23cac11e8bd5bc641797dd72752..7caa9958361f92711bf8e2d347f1116a13a7fe58 100644 (file)
@@ -46,7 +46,7 @@ public class MapLoadBenchmark
         PoolManager.Shutdown();
     }
 
-    public static IEnumerable<string> 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;
index 65b5abaf731b7ef7f542b69e14a3176b3522dc5c..0beb0a613d5ed35af481bfae4e5622276d0e4ca9 100644 (file)
@@ -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<IPrototypeManager>().EnumeratePrototypes<GameMapPrototype>().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 (file)
index 0000000..c7f22bd
--- /dev/null
@@ -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<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>();
+
+        // Spawn the map
+        _pair.Server.ResolveDependency<IRobustRandom>().SetSeed(42);
+        _pair.Server.WaitPost(() =>
+        {
+            var success = _entMan.System<MapLoaderSystem>().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<WarpPointComponent>()
+            .OrderBy(x => x.Component.Location)
+            .Select(x => _entMan.GetComponent<TransformComponent>(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<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);
+    }
+
+    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<EntityUid> ViewSubscriptions { get; } = new();
+        public DateTime ConnectedTime { get; set; }
+        public SessionState State => default!;
+        public SessionData Data => default!;
+        public bool ClientSide { get; set; }
+    }
+}