Damageable/Destructible Benchmarks (#41064)

* the fard

* oomba

* The woke swarm...

* Review

* review

* Apply suggestions from code review

---------

Co-authored-by: Princess Cheeseballs <66055347+Pronana@users.noreply.github.com>
Co-authored-by: slarticodefast <161409025+slarticodefast@users.noreply.github.com>
This commit is contained in:
Princess Cheeseballs
2025-10-26 19:46:27 -07:00
committed by GitHub
parent 7dbf084940
commit 3a9bcf1a83
5 changed files with 211 additions and 2 deletions

View File

@@ -0,0 +1,159 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading.Tasks;
using BenchmarkDotNet.Attributes;
using Content.IntegrationTests;
using Content.IntegrationTests.Pair;
using Content.Server.Destructible;
using Content.Shared.Damage;
using Content.Shared.Damage.Prototypes;
using Content.Shared.FixedPoint;
using Content.Shared.Maps;
using Robust.Shared;
using Robust.Shared.Analyzers;
using Robust.Shared.GameObjects;
using Robust.Shared.Map;
using Robust.Shared.Maths;
using Robust.Shared.Prototypes;
using Robust.Shared.Random;
namespace Content.Benchmarks;
[Virtual]
[GcServer(true)]
[MemoryDiagnoser]
public class DestructibleBenchmark
{
/// <summary>
/// Number of destructible entities per prototype to spawn with a <see cref="DestructibleComponent"/>.
/// </summary>
[Params(1, 10, 100, 1000, 5000)]
public int EntityCount;
/// <summary>
/// Amount of blunt damage we do to each entity.
/// </summary>
[Params(10000)]
public FixedPoint2 DamageAmount;
[Params("Blunt")]
public ProtoId<DamageTypePrototype> DamageType;
private static readonly EntProtoId WindowProtoId = "Window";
private static readonly EntProtoId WallProtoId = "WallReinforced";
private static readonly EntProtoId HumanProtoId = "MobHuman";
private static readonly ProtoId<ContentTileDefinition> TileRef = "Plating";
private readonly EntProtoId[] _prototypes = [WindowProtoId, WallProtoId, HumanProtoId];
private readonly List<Entity<DamageableComponent>> _damageables = new();
private readonly List<Entity<DamageableComponent, DestructibleComponent>> _destructbiles = new();
private DamageSpecifier _damage;
private TestPair _pair = default!;
private IEntityManager _entMan = default!;
private IPrototypeManager _protoMan = default!;
private IRobustRandom _random = default!;
private ITileDefinitionManager _tileDefMan = default!;
private DamageableSystem _damageable = default!;
private DestructibleSystem _destructible = default!;
private SharedMapSystem _map = default!;
[GlobalSetup]
public async Task SetupAsync()
{
ProgramShared.PathOffset = "../../../../";
PoolManager.Startup();
_pair = await PoolManager.GetServerClient();
var server = _pair.Server;
var mapdata = await _pair.CreateTestMap();
_entMan = server.ResolveDependency<IEntityManager>();
_protoMan = server.ResolveDependency<IPrototypeManager>();
_random = server.ResolveDependency<IRobustRandom>();
_tileDefMan = server.ResolveDependency<ITileDefinitionManager>();
_damageable = _entMan.System<DamageableSystem>();
_destructible = _entMan.System<DestructibleSystem>();
_map = _entMan.System<SharedMapSystem>();
if (!_protoMan.Resolve(DamageType, out var type))
return;
_damage = new DamageSpecifier(type, DamageAmount);
_random.SetSeed(69420); // Randomness needs to be deterministic for benchmarking.
var plating = _tileDefMan[TileRef].TileId;
// We make a rectangular grid of destructible entities, and then damage them all simultaneously to stress test the system.
// Needed for managing the performance of destructive effects and damage application.
await server.WaitPost(() =>
{
// Set up a thin line of tiles to place our objects on. They should be anchored for a "realistic" scenario...
for (var x = 0; x < EntityCount; x++)
{
for (var y = 0; y < _prototypes.Length; y++)
{
_map.SetTile(mapdata.Grid, mapdata.Grid, new Vector2i(x, y), new Tile(plating));
}
}
for (var x = 0; x < EntityCount; x++)
{
var y = 0;
foreach (var protoId in _prototypes)
{
var coords = new EntityCoordinates(mapdata.Grid, x + 0.5f, y + 0.5f);
_entMan.SpawnEntity(protoId, coords);
y++;
}
}
var query = _entMan.EntityQueryEnumerator<DamageableComponent, DestructibleComponent>();
while (query.MoveNext(out var uid, out var damageable, out var destructible))
{
_damageables.Add((uid, damageable));
_destructbiles.Add((uid, damageable, destructible));
}
});
}
[Benchmark]
public async Task PerformDealDamage()
{
await _pair.Server.WaitPost(() =>
{
_damageable.ApplyDamageToAllEntities(_damageables, _damage);
});
}
[Benchmark]
public async Task PerformTestTriggers()
{
await _pair.Server.WaitPost(() =>
{
_destructible.TestAllTriggers(_destructbiles);
});
}
[Benchmark]
public async Task PerformTestBehaviors()
{
await _pair.Server.WaitPost(() =>
{
_destructible.TestAllBehaviors(_destructbiles);
});
}
[GlobalCleanup]
public async Task CleanupAsync()
{
await _pair.DisposeAsync();
PoolManager.Shutdown();
}
}

View File

@@ -0,0 +1,35 @@
using Content.Shared.Damage;
namespace Content.Server.Destructible;
public sealed partial class DestructibleSystem
{
/// <summary>
/// Tests all triggers in a DestructibleComponent to see how expensive it is to query them.
/// </summary>
public void TestAllTriggers(List<Entity<DamageableComponent, DestructibleComponent>> destructibles)
{
foreach (var (uid, damageable, destructible) in destructibles)
{
foreach (var threshold in destructible.Thresholds)
{
// Chances are, none of these triggers will pass!
Triggered(threshold, (uid, damageable));
}
}
}
/// <summary>
/// Tests all behaviours in a DestructibleComponent to see how expensive it is to query them.
/// </summary>
public void TestAllBehaviors(List<Entity<DamageableComponent, DestructibleComponent>> destructibles)
{
foreach (var (uid, damageable, destructible) in destructibles)
{
foreach (var threshold in destructible.Thresholds)
{
Execute(threshold, uid);
}
}
}
}

View File

@@ -26,7 +26,7 @@ using Robust.Shared.Random;
namespace Content.Server.Destructible
{
[UsedImplicitly]
public sealed class DestructibleSystem : SharedDestructibleSystem
public sealed partial class DestructibleSystem : SharedDestructibleSystem
{
[Dependency] public readonly IRobustRandom Random = default!;
public new IEntityManager EntityManager => base.EntityManager;

View File

@@ -0,0 +1,15 @@
namespace Content.Shared.Damage;
public sealed partial class DamageableSystem
{
/// <summary>
/// Applies damage to all entities to see how expensive it is to deal damage.
/// </summary>
public void ApplyDamageToAllEntities(List<Entity<DamageableComponent>> damageables, DamageSpecifier damage)
{
foreach (var (uid, damageable) in damageables)
{
TryChangeDamage(uid, damage, damageable: damageable);
}
}
}

View File

@@ -17,7 +17,7 @@ using Robust.Shared.Utility;
namespace Content.Shared.Damage
{
public sealed class DamageableSystem : EntitySystem
public sealed partial class DamageableSystem : EntitySystem
{
[Dependency] private readonly IPrototypeManager _prototypeManager = default!;
[Dependency] private readonly SharedAppearanceSystem _appearance = default!;