Profile and reduce daemon simulation hot paths

This commit is contained in:
Meta Vens 2026-07-19 10:00:18 +01:00
commit 289051f71a
3 changed files with 93 additions and 17 deletions

View file

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
#nullable enable
@ -31,6 +32,18 @@ namespace GameCult.Aetheria.State.Verse
if (worldPhysics == null)
throw new ArgumentNullException(nameof(worldPhysics));
var traceSimulationPhases = string.Equals(
Environment.GetEnvironmentVariable("AETHERIA_TRACE_SIMULATION_PHASES"),
"1",
StringComparison.Ordinal);
var phase = Stopwatch.StartNew();
void TracePhase(string name)
{
if (traceSimulationPhases && phase.ElapsedMilliseconds >= 20)
Console.WriteLine($"Aetheria simulation phase {name} took {phase.ElapsedMilliseconds}ms.");
phase.Restart();
}
StepWormholeTransitions(
run,
intents == null
@ -46,6 +59,7 @@ namespace GameCult.Aetheria.State.Verse
.Where(zone => zone != null)
.Select(zone => zone.ZoneIndex)
.ToArray());
TracePhase("world-retention");
foreach (var zone in run.Zones ?? Array.Empty<AetheriaRuntimeZoneSnapshotCommit>())
{
@ -93,12 +107,16 @@ namespace GameCult.Aetheria.State.Verse
settings.TorqueFloor,
settings.TorqueMultiplier);
StepTractorPower(entities, deltaSeconds);
TracePhase($"zone-{zone.ZoneIndex}-pre-physics");
var worldStep = StepWorldPhysics(
run.RunId, frameId, simulationStepIndex, zone, entities, deltaSeconds, worldPhysics);
TracePhase($"zone-{zone.ZoneIndex}-ymir");
ProjectEntityTerrainHeights(zone, entities, catalog, simulationTimeSeconds);
ResolvePickupProximity(run, zone, entities, catalog, frameId);
TracePhase($"zone-{zone.ZoneIndex}-terrain-pickup");
StepCombat(run, zone, entities, intents, deltaSeconds, settings, worldPhysics, catalog,
frameId, simulationTimeSeconds, simulationStepIndex);
TracePhase($"zone-{zone.ZoneIndex}-combat");
AetheriaRuntimeVisibilitySimulation.StepZone(zone, entities, catalog, renderSettings);
var activeRenderSettings = renderSettings ?? AetheriaRuntimeDaemonRenderSettings.AetheriaDefault;
AetheriaRuntimeSensorSimulation.StepZone(
@ -114,7 +132,9 @@ namespace GameCult.Aetheria.State.Verse
settings.SecureAreaRadiusMultiplier);
AetheriaRuntimeMiningSimulation.Step(run, zone, entities, intents, catalog, frameId, simulationTimeSeconds, deltaSeconds);
AetheriaRuntimeSurveySimulation.Step(run, zone, entities, intents, catalog, frameId, simulationTimeSeconds, deltaSeconds);
TracePhase($"zone-{zone.ZoneIndex}-sensors-resources");
FinalizeBehaviorChainsAndThermal(run, zone, entities, catalog, deltaSeconds, settings, frameId);
TracePhase($"zone-{zone.ZoneIndex}-thermal-finalize");
}
}
@ -578,12 +598,15 @@ namespace GameCult.Aetheria.State.Verse
foreach (var attacker in combatEntities)
{
var requestedWeapons = ResolveWeaponTriggers(zone, attacker, entities, intents, catalog);
StepDeployableWeapons(run, zone, attacker, requestedWeapons, deltaSeconds, settings, catalog, frameId);
var operationalBehaviors = AetheriaRuntimeEquippedBehaviorQueries
.FindAllOperational(attacker, catalog);
StepDeployableWeapons(run, zone, attacker, operationalBehaviors, requestedWeapons,
deltaSeconds, settings, catalog, frameId);
StepChargedWeapons(run, zone, attacker, byIndex, requestedWeapons, deltaSeconds,
settings, catalog, frameId);
operationalBehaviors, settings, catalog, frameId);
StepConstantWeapons(run, zone, byIndex, attacker, requestedWeapons, deltaSeconds,
settings, catalog, frameId);
var weapons = ResolveWeapons(attacker, catalog, settings);
operationalBehaviors, settings, catalog, frameId);
var weapons = ResolveWeapons(attacker, operationalBehaviors, catalog, settings);
foreach (var weapon in weapons)
{
var autoTriggerReady =
@ -726,13 +749,15 @@ namespace GameCult.Aetheria.State.Verse
AetheriaRuntimeRunCheckpointCommit run,
AetheriaRuntimeZoneSnapshotCommit zone,
AetheriaRuntimeEntitySnapshotCommit attacker,
IReadOnlyList<AetheriaRuntimeEquippedBehavior> operationalBehaviors,
WeaponTriggerSet requestedWeapons,
double deltaSeconds,
AetheriaRuntimeDaemonSimulationSettings settings,
AetheriaRuntimeCatalogSnapshot? catalog,
long frameId)
{
foreach (var deployable in ResolveDeployableWeapons(attacker, requestedWeapons, catalog, settings))
foreach (var deployable in ResolveDeployableWeapons(
attacker, operationalBehaviors, requestedWeapons, settings))
{
var weapon = deployable.Weapon;
weapon.State.Firing = false;
@ -882,11 +907,13 @@ namespace GameCult.Aetheria.State.Verse
IReadOnlyDictionary<int, AetheriaRuntimeEntitySnapshotCommit> byIndex,
WeaponTriggerSet requestedWeapons,
double deltaSeconds,
IReadOnlyList<AetheriaRuntimeEquippedBehavior> operationalBehaviors,
AetheriaRuntimeDaemonSimulationSettings settings,
AetheriaRuntimeCatalogSnapshot? catalog,
long frameId)
{
foreach (var weapon in ResolveChargedWeapons(attacker, requestedWeapons, catalog, settings))
foreach (var weapon in ResolveChargedWeapons(
attacker, operationalBehaviors, requestedWeapons, settings))
{
var state = weapon.Base.State;
state.Firing = false;
@ -1075,11 +1102,13 @@ namespace GameCult.Aetheria.State.Verse
AetheriaRuntimeEntitySnapshotCommit attacker,
WeaponTriggerSet requestedWeapons,
double deltaSeconds,
IReadOnlyList<AetheriaRuntimeEquippedBehavior> operationalBehaviors,
AetheriaRuntimeDaemonSimulationSettings settings,
AetheriaRuntimeCatalogSnapshot? catalog,
long frameId)
{
foreach (var weapon in ResolveConstantWeapons(attacker, requestedWeapons, catalog, settings))
foreach (var weapon in ResolveConstantWeapons(
attacker, operationalBehaviors, requestedWeapons, settings))
{
if (weapon.State.Reloading)
{
@ -1515,11 +1544,13 @@ namespace GameCult.Aetheria.State.Verse
private static IReadOnlyList<ResolvedConstantWeapon> ResolveConstantWeapons(
AetheriaRuntimeEntitySnapshotCommit entity,
IReadOnlyList<AetheriaRuntimeEquippedBehavior> operationalBehaviors,
WeaponTriggerSet requestedWeapons,
AetheriaRuntimeCatalogSnapshot? catalog,
AetheriaRuntimeDaemonSimulationSettings settings)
{
return AetheriaRuntimeEquippedBehaviorQueries.FindOperational(entity, catalog, AetheriaRuntimeBehaviorKinds.ConstantWeapon)
return operationalBehaviors
.Where(behavior => AetheriaRuntimeBehaviorMetadataCatalog.IsKindOrDescendant(
behavior.Payload.Kind, AetheriaRuntimeBehaviorKinds.ConstantWeapon))
.Select(behavior =>
{
var magazineSize = Math.Max(0, (int)Math.Round(ReadNumber(behavior.Payload, 13)));
@ -1546,11 +1577,13 @@ namespace GameCult.Aetheria.State.Verse
private static IReadOnlyList<ResolvedDeployableWeapon> ResolveDeployableWeapons(
AetheriaRuntimeEntitySnapshotCommit entity,
IReadOnlyList<AetheriaRuntimeEquippedBehavior> operationalBehaviors,
WeaponTriggerSet requestedWeapons,
AetheriaRuntimeCatalogSnapshot? catalog,
AetheriaRuntimeDaemonSimulationSettings settings)
{
return AetheriaRuntimeEquippedBehaviorQueries.FindOperational(entity, catalog, AetheriaRuntimeBehaviorKinds.DeployableWeapon)
return operationalBehaviors
.Where(behavior => AetheriaRuntimeBehaviorMetadataCatalog.IsKindOrDescendant(
behavior.Payload.Kind, AetheriaRuntimeBehaviorKinds.DeployableWeapon))
.Where(behavior => requestedWeapons.ContainsPulse(behavior.EquipmentIndex, behavior.BehaviorIndex))
.Select(behavior => new ResolvedDeployableWeapon(
ResolveAuthoredWeapon(entity, behavior, settings),
@ -1910,10 +1943,14 @@ namespace GameCult.Aetheria.State.Verse
}
private static IReadOnlyList<ResolvedChargedWeapon> ResolveChargedWeapons(
AetheriaRuntimeEntitySnapshotCommit entity, WeaponTriggerSet requestedWeapons,
AetheriaRuntimeCatalogSnapshot? catalog, AetheriaRuntimeDaemonSimulationSettings settings)
AetheriaRuntimeEntitySnapshotCommit entity,
IReadOnlyList<AetheriaRuntimeEquippedBehavior> operationalBehaviors,
WeaponTriggerSet requestedWeapons,
AetheriaRuntimeDaemonSimulationSettings settings)
{
return AetheriaRuntimeEquippedBehaviorQueries.FindOperational(entity, catalog, AetheriaRuntimeBehaviorKinds.ChargedWeapon)
return operationalBehaviors
.Where(behavior => AetheriaRuntimeBehaviorMetadataCatalog.IsKindOrDescendant(
behavior.Payload.Kind, AetheriaRuntimeBehaviorKinds.ChargedWeapon))
.Where(value => string.Equals(value.Payload.Kind, AetheriaRuntimeBehaviorKinds.ChargedWeapon, StringComparison.Ordinal))
.Select(value => new ResolvedChargedWeapon(
ResolveAuthoredWeapon(entity, value, settings), value.Item,
@ -1929,10 +1966,13 @@ namespace GameCult.Aetheria.State.Verse
private static IReadOnlyList<ResolvedWeapon> ResolveWeapons(
AetheriaRuntimeEntitySnapshotCommit entity,
IReadOnlyList<AetheriaRuntimeEquippedBehavior> operationalBehaviors,
AetheriaRuntimeCatalogSnapshot? catalog,
AetheriaRuntimeDaemonSimulationSettings settings)
{
var authored = AetheriaRuntimeEquippedBehaviorQueries.FindOperational(entity, catalog, AetheriaRuntimeBehaviorKinds.InstantWeapon)
var authored = operationalBehaviors
.Where(behavior => AetheriaRuntimeBehaviorMetadataCatalog.IsKindOrDescendant(
behavior.Payload.Kind, AetheriaRuntimeBehaviorKinds.InstantWeapon))
.Where(behavior => !string.Equals(behavior.Payload.Kind, AetheriaRuntimeBehaviorKinds.ChargedWeapon, StringComparison.Ordinal))
.Where(behavior => !string.Equals(behavior.Payload.Kind, AetheriaRuntimeBehaviorKinds.DeployableWeapon, StringComparison.Ordinal))
.Select(behavior => ResolveAuthoredWeapon(entity, behavior, settings))

View file

@ -81,7 +81,20 @@ namespace GameCult.Aetheria.State.Verse
AetheriaRuntimeCatalogSnapshot? catalog,
string behaviorKind)
{
if (entity == null || catalog == null || string.IsNullOrWhiteSpace(behaviorKind))
if (string.IsNullOrWhiteSpace(behaviorKind))
return Array.Empty<AetheriaRuntimeEquippedBehavior>();
return FindAll(entity, catalog)
.Where(value => AetheriaRuntimeBehaviorMetadataCatalog.IsKindOrDescendant(
value.Payload.Kind, behaviorKind))
.ToArray();
}
public static IReadOnlyList<AetheriaRuntimeEquippedBehavior> FindAll(
AetheriaRuntimeEntitySnapshotCommit? entity,
AetheriaRuntimeCatalogSnapshot? catalog)
{
if (entity == null || catalog == null)
return Array.Empty<AetheriaRuntimeEquippedBehavior>();
AetheriaRuntimeBehaviorStateProjector.EnsureEquipmentBehaviorStates(entity, catalog);
@ -102,7 +115,6 @@ namespace GameCult.Aetheria.State.Verse
{
var payload = payloads[behaviorIndex];
if (payload == null ||
!AetheriaRuntimeBehaviorMetadataCatalog.IsKindOrDescendant(payload.Kind, behaviorKind) ||
!states.TryGetValue((equipmentIndex, behaviorIndex), out var state))
{
continue;
@ -139,6 +151,22 @@ namespace GameCult.Aetheria.State.Verse
.ToArray();
}
public static IReadOnlyList<AetheriaRuntimeEquippedBehavior> FindAllOperational(
AetheriaRuntimeEntitySnapshotCommit? entity,
AetheriaRuntimeCatalogSnapshot? catalog)
{
if (entity == null)
return Array.Empty<AetheriaRuntimeEquippedBehavior>();
var equipmentStates = (entity.EquipmentStates ?? Array.Empty<AetheriaRuntimeEquipmentStateCommit>())
.Where(state => state != null)
.ToDictionary(state => state.EquipmentIndex);
return FindAll(entity, catalog)
.Where(value => value.Item.Enabled && value.Item.Durability > 0.01 &&
(!equipmentStates.TryGetValue(value.EquipmentIndex, out var state) || state.Online))
.ToArray();
}
public static IReadOnlyList<AetheriaRuntimeEquippedBehavior> FindExecuting(
AetheriaRuntimeEntitySnapshotCommit? entity,
AetheriaRuntimeCatalogSnapshot? catalog,

View file

@ -360,6 +360,14 @@ Thermotoggle target temperature is also daemon-owned behavior state. Authored
data seeds it, adjustable commands mutate it, and its high/low-pass predicate
gates the same ordered chain.
Within one combat step, authoritative equipment, online state, durability, and
catalog payloads are read once into a tick-local operational behavior set.
Deployable, charged, constant, and instant weapon projections derive from that
same set. It is not persisted and cannot outlive the step; mutations still land
on the referenced authoritative item and behavior state. This prevents each
weapon family from independently rebuilding the same equipment graph without
introducing a cache owner beside the entity snapshot.
Migration target:
- blueprint/catalog rows define base stat ids and default values;