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!;