diff --git a/Content.Benchmarks/DestructibleBenchmark.cs b/Content.Benchmarks/DestructibleBenchmark.cs new file mode 100644 index 0000000000..b91837e7ca --- /dev/null +++ b/Content.Benchmarks/DestructibleBenchmark.cs @@ -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 +{ + /// + /// Number of destructible entities per prototype to spawn with a . + /// + [Params(1, 10, 100, 1000, 5000)] + public int EntityCount; + + /// + /// Amount of blunt damage we do to each entity. + /// + [Params(10000)] + public FixedPoint2 DamageAmount; + + [Params("Blunt")] + public ProtoId DamageType; + + private static readonly EntProtoId WindowProtoId = "Window"; + private static readonly EntProtoId WallProtoId = "WallReinforced"; + private static readonly EntProtoId HumanProtoId = "MobHuman"; + + private static readonly ProtoId TileRef = "Plating"; + + private readonly EntProtoId[] _prototypes = [WindowProtoId, WallProtoId, HumanProtoId]; + + private readonly List> _damageables = new(); + private readonly List> _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(); + _protoMan = server.ResolveDependency(); + _random = server.ResolveDependency(); + _tileDefMan = server.ResolveDependency(); + _damageable = _entMan.System(); + _destructible = _entMan.System(); + _map = _entMan.System(); + + 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(); + + 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(); + } +} diff --git a/Content.Server/Destructible/DestructibleSystem.BenchmarkHelpers.cs b/Content.Server/Destructible/DestructibleSystem.BenchmarkHelpers.cs new file mode 100644 index 0000000000..ac5e3704fe --- /dev/null +++ b/Content.Server/Destructible/DestructibleSystem.BenchmarkHelpers.cs @@ -0,0 +1,35 @@ +using Content.Shared.Damage; + +namespace Content.Server.Destructible; + +public sealed partial class DestructibleSystem +{ + /// + /// Tests all triggers in a DestructibleComponent to see how expensive it is to query them. + /// + public void TestAllTriggers(List> 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)); + } + } + } + + /// + /// Tests all behaviours in a DestructibleComponent to see how expensive it is to query them. + /// + public void TestAllBehaviors(List> destructibles) + { + foreach (var (uid, damageable, destructible) in destructibles) + { + foreach (var threshold in destructible.Thresholds) + { + Execute(threshold, uid); + } + } + } +} diff --git a/Content.Server/Destructible/DestructibleSystem.cs b/Content.Server/Destructible/DestructibleSystem.cs index 682baa04ca..847229278c 100644 --- a/Content.Server/Destructible/DestructibleSystem.cs +++ b/Content.Server/Destructible/DestructibleSystem.cs @@ -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; diff --git a/Content.Shared/Damage/Systems/DamageableSystem.BenchmarkHelpers.cs b/Content.Shared/Damage/Systems/DamageableSystem.BenchmarkHelpers.cs new file mode 100644 index 0000000000..d248d717b8 --- /dev/null +++ b/Content.Shared/Damage/Systems/DamageableSystem.BenchmarkHelpers.cs @@ -0,0 +1,15 @@ +namespace Content.Shared.Damage; + +public sealed partial class DamageableSystem +{ + /// + /// Applies damage to all entities to see how expensive it is to deal damage. + /// + public void ApplyDamageToAllEntities(List> damageables, DamageSpecifier damage) + { + foreach (var (uid, damageable) in damageables) + { + TryChangeDamage(uid, damage, damageable: damageable); + } + } +} diff --git a/Content.Shared/Damage/Systems/DamageableSystem.cs b/Content.Shared/Damage/Systems/DamageableSystem.cs index b849227156..f1e259001c 100644 --- a/Content.Shared/Damage/Systems/DamageableSystem.cs +++ b/Content.Shared/Damage/Systems/DamageableSystem.cs @@ -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!;