Files
space-station-14/Content.Benchmarks/PvsBenchmark.cs
T
Moony d42adbf05d Gametest Part 2: Preliminary refactor every test to use GameTest as the framework. (#43207)
* Pass 1.

* i'm FREE

* Prevent hangups.

* okay fine here's an attribute for settings, will polish later and prolly remove the overridable thing.

* sigh.

* fix singular trigger bug so LatheTest doesn't flake.

* Remove SystemAttribute usage.

* Poke

* I used the shotgun. You know why? Cause the shot gun doesn’t miss, and unlike the shitty hybrid taser it stops a criminal in their tracks in two hits. Bang, bang, and they’re fucking done. I use four shots just to make damn sure. Because, once again, I’m not there to coddle a buncha criminal scum sucking f------, I’m there to 1) Survive the fucking round. 2) Guard the armory. So you can absolutely get fucked. If I get unbanned, which I won’t, you can guarantee I will continue to use the shotgun to apprehend criminals. Because it’s quick, clean and effective as fuck. Why in the seven hells would I fuck around with the disabler shots, which take half a clip just to bring someone down, or with the tazer bolts which are slow as balls, impossible to aim and do about next to jack shit, fuck all. The shotgun is the superior law enforcement weapon. Because it stops crime. And it stops crime by reducing the number of criminals roaming the fucking halls.

* Change the faulty store test into two tests, one of which is ignored for failing.
2026-04-01 16:06:26 +00:00

179 lines
6.1 KiB
C#

#nullable enable
using System;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using BenchmarkDotNet.Attributes;
using Content.IntegrationTests;
using Content.IntegrationTests.Pair;
using Content.Server.Mind;
using Content.Shared.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(testContext: new ExternalTestContext("Benchmark", StreamWriter.Null)).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);
}
}