Add PVS benchmark (#23166)
* Add PVS benchmark * poke tests * Shuffle players around * Add caveat * Add CycleTick() benchmark * Make async false * Oops
This commit is contained in:
@@ -46,7 +46,7 @@ public class MapLoadBenchmark
|
|||||||
PoolManager.Shutdown();
|
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))]
|
[ParamsSource(nameof(MapsSource))]
|
||||||
public string Map;
|
public string Map;
|
||||||
|
|||||||
@@ -23,13 +23,6 @@ namespace Content.Benchmarks
|
|||||||
|
|
||||||
public static async Task MainAsync(string[] args)
|
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
|
#if DEBUG
|
||||||
Console.ForegroundColor = ConsoleColor.Red;
|
Console.ForegroundColor = ConsoleColor.Red;
|
||||||
Console.WriteLine("\nWARNING: YOU ARE RUNNING A DEBUG BUILD, USE A RELEASE BUILD FOR AN ACCURATE BENCHMARK");
|
Console.WriteLine("\nWARNING: YOU ARE RUNNING A DEBUG BUILD, USE A RELEASE BUILD FOR AN ACCURATE BENCHMARK");
|
||||||
|
|||||||
187
Content.Benchmarks/PvsBenchmark.cs
Normal file
187
Content.Benchmarks/PvsBenchmark.cs
Normal file
@@ -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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user