Skip to content

Comments

performance: defer cache rebuilds and reduce allocations in MapInstance#2810

Closed
Arufonsu wants to merge 1 commit intomainfrom
performance/mapinstance-cache-and-alloc-reduction
Closed

performance: defer cache rebuilds and reduce allocations in MapInstance#2810
Arufonsu wants to merge 1 commit intomainfrom
performance/mapinstance-cache-and-alloc-reduction

Conversation

@Arufonsu
Copy link
Contributor

Replace per-mutation cache rebuilds with dirty-flag deferred flushing across entities, projectiles, and traps in MapInstance, and cache surrounding-instance lookups to eliminate per-tick allocations.

  • Add _entityCacheDirty / _projectileCacheDirty / _trapCacheDirty flags; caches are now rebuilt once per tick in FlushDirtyCaches() called at the top of Update(), not on every Add/Remove call
  • Cache surrounding MapInstance lists (_cachedSurroundingInstances / _cachedSurroundingInstancesWithSelf) with an invalidation flag; replace all MapController.GetSurroundingMapInstances() call sites with GetCachedSurroundingInstances() to avoid allocating a new List<> on every tick
  • Add _entityListLock to decouple entity-list mutations from mMapProcessLock, reducing lock contention during the update cycle
  • Replace mIsProcessing = GetPlayers(true).Any() with a direct check over cached surrounding instances to avoid double allocation
  • Use ArrayPool.Shared in DespawnProjectiles() to avoid a per-call List and ToArray()
  • Snapshot NpcSpawnInstances/mEntities before acquiring EntityLocks in ResetNpcSpawns() and DespawnNpcs() to eliminate nested-lock deadlock risk
  • Replace GetEntities() call sites in hot paths (IsTileBlocked, UpdateResources, ClearEntityTargetsOf) with direct mCachedEntities array iteration
  • Use GlobalEventInstances.Values directly in UpdateGlobalEvents() instead of ToList(); short-circuit active search with break
  • Lazy-init toRemove list in SpawnItem(): no allocation in the common zero-match case
  • Replace ContainsKey+indexer double-lookup with TryGetValue in GetGlobalEventInstance() and FindEvent()
  • Invert null-guard conditions to early-return (SendMapEntitiesTo, RemoveItem, SpawnItems, UpdateResources) for reduced nesting
  • Replace if/else attribute type checks with switch expressions in SpawnMapAttributes() and CacheMapBlocks()
  • Collapse trivial single-expression methods to expression bodies (GetLock, GetController, ShouldBeCleaned, ShouldBeActive, GetCachedEntities, GetCachedBlocks, AddBatched*)
  • Remove GetControllerLock(): unused after refactor
  • Trim verbose XML doc comments to concise inline remarks; remove redundant using Intersect.Utilities

Replace per-mutation cache rebuilds with dirty-flag deferred flushing
across entities, projectiles, and traps in MapInstance, and cache
surrounding-instance lookups to eliminate per-tick allocations.

- Add _entityCacheDirty / _projectileCacheDirty / _trapCacheDirty flags;
  caches are now rebuilt once per tick in FlushDirtyCaches() called at
  the top of Update(), not on every Add/Remove call
- Cache surrounding MapInstance lists (_cachedSurroundingInstances /
  _cachedSurroundingInstancesWithSelf) with an invalidation flag;
  replace all MapController.GetSurroundingMapInstances() call sites with
  GetCachedSurroundingInstances() to avoid allocating a new List<> on
  every tick
- Add _entityListLock to decouple entity-list mutations from
  mMapProcessLock, reducing lock contention during the update cycle
- Replace mIsProcessing = GetPlayers(true).Any() with a direct check
  over cached surrounding instances to avoid double allocation
- Use ArrayPool<Guid>.Shared in DespawnProjectiles() to avoid a
  per-call List<Guid> and ToArray()
- Snapshot NpcSpawnInstances/mEntities before acquiring EntityLocks in
  ResetNpcSpawns() and DespawnNpcs() to eliminate nested-lock deadlock risk
- Replace GetEntities() call sites in hot paths (IsTileBlocked,
  UpdateResources, ClearEntityTargetsOf) with direct mCachedEntities array iteration
- Use GlobalEventInstances.Values directly in UpdateGlobalEvents()
  instead of ToList(); short-circuit active search with break
- Lazy-init toRemove list in SpawnItem(): no allocation in the common
  zero-match case
- Replace ContainsKey+indexer double-lookup with TryGetValue in
  GetGlobalEventInstance() and FindEvent()
- Invert null-guard conditions to early-return (SendMapEntitiesTo,
  RemoveItem, SpawnItems, UpdateResources) for reduced nesting
- Replace if/else attribute type checks with switch expressions in
  SpawnMapAttributes() and CacheMapBlocks()
- Collapse trivial single-expression methods to expression bodies
  (GetLock, GetController, ShouldBeCleaned, ShouldBeActive,
  GetCachedEntities, GetCachedBlocks, AddBatched*)
- Remove GetControllerLock(): unused after refactor
- Trim verbose XML doc comments to concise inline remarks; remove
  redundant using Intersect.Utilities

Signed-off-by: Arufonsu <17498701+Arufonsu@users.noreply.github.com>
@Arufonsu Arufonsu requested review from a team February 22, 2026 00:18
@Arufonsu Arufonsu added chore Cleans up code, documentation or project structure without altering functionality performance Performance optimization labels Feb 22, 2026
Copy link
Member

@pandinocoder pandinocoder left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure what's going on with this PR but I'm not done reviewing it. I suspect my existing comments apply to more than the stuff I read though.

Comment on lines +169 to +170
private List<MapInstance> _cachedSurroundingInstances = new List<MapInstance>();
private List<MapInstance> _cachedSurroundingInstancesWithSelf = new List<MapInstance>();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Prefer = []; syntax for non-Array collections

// Cached surrounding-instance lists rebuilt only when dirty.
private List<MapInstance> _cachedSurroundingInstances = new List<MapInstance>();
private List<MapInstance> _cachedSurroundingInstancesWithSelf = new List<MapInstance>();
private bool _surroundingCacheDirty = true;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this not volatile when the others are?


public bool IsDisposed { get; protected set; }

/// <summary>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why did you gut a doc comment...?

/// Initializes the processing instance for processing - called in the constructor. Essentially refreshes the instance
/// so that it can give everything it has to offer to the player by the time they arrive in it
/// </summary>
// Invalidate the surrounding-instance cache when grid/instance topology changes.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't a doc comment

// Invalidate the surrounding-instance cache when grid/instance topology changes.
public void InvalidateSurroundingCache() => _surroundingCacheDirty = true;

// Returns a cached list of surrounding MapInstances (only rebuilt when dirty).
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't a doc comment

{
dir = Randomization.NextDirection();
}
dir = spawn.Direction != NpcSpawnDirection.Random ? (Direction)(spawn.Direction - 1) : Randomization.NextDirection();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is less readable than the if statements and gives no performance benefit

MapInstanceId = processLayer
};

var npc = new Npc(npcBase, despawnable) { MapId = mMapController.Id, X = tileX, Y = tileY, Dir = dir, MapInstanceId = MapInstanceId };
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This shouldn't be squished into one line.


/// <summary>
/// Forcibly reset all Npcs originating from this map.
/// Snapshot spawn list before acquiring EntityLocks to avoid nested-lock deadlock.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why did you replace a proper doc comment with a detail comment that also doesn't appear to be relevant to the method operation and probably should be inside the method instead.

/// </summary>
public void ResetNpcSpawns()
{
//Kill all npcs spawned from this map
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should move the comment down to where it's relevant


/// <summary>
/// Despawns all NPCs, and removes them from our dictionary of Spawn Instances.
/// Snapshot before EntityLocks to avoid nested-lock deadlock.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

??????????????

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ai slop chore Cleans up code, documentation or project structure without altering functionality performance Performance optimization

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants