Skip to content

Comments

performance: reduce server allocations in LogicThread hot path#2809

Closed
Arufonsu wants to merge 1 commit intomainfrom
performance/logicthread-alloc-reduction
Closed

performance: reduce server allocations in LogicThread hot path#2809
Arufonsu wants to merge 1 commit intomainfrom
performance/logicthread-alloc-reduction

Conversation

@Arufonsu
Copy link
Contributor

Reduces several per-tick and per-second heap allocations in LogicService.LogicThread to reduce GC pressure at runtime.

  • Replace ActiveMapInstances.ToArray() with a reusable MapInstance[] buffer (_activeMapInstancesBuffer); reallocate only when Count changes
  • Collect map instance removals in a reusable List (toRemove) declared outside the while-loop instead of removing during enumeration
  • Replace .Where(...).Count() LINQ chain with a plain foreach loop when counting active event call stacks in the metrics block
  • Replace ActiveMapInstances.Keys.Contains() with ContainsKey() in all three call sites (direct dictionary lookup, O(1) vs O(n))
  • Guard Console.Title assignment behind a string equality check using _lastConsoleTitle; avoids redundant Win32/syscall every second when server state is unchanged
  • Remove unused using Intersect.Utilities import

Reduces several per-tick and per-second heap allocations in
LogicService.LogicThread to reduce GC pressure at runtime.

- Replace ActiveMapInstances.ToArray() with a reusable MapInstance[]
  buffer (_activeMapInstancesBuffer); reallocate only when Count changes
- Collect map instance removals in a reusable List<Guid> (toRemove)
  declared outside the while-loop instead of removing during enumeration
- Replace .Where(...).Count() LINQ chain with a plain foreach loop when
  counting active event call stacks in the metrics block
- Replace ActiveMapInstances.Keys.Contains() with ContainsKey() in all
  three call sites (direct dictionary lookup, O(1) vs O(n))
- Guard Console.Title assignment behind a string equality check using
  _lastConsoleTitle; avoids redundant Win32/syscall every second when
  server state is unchanged
- Remove unused `using Intersect.Utilities` import

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

//Refresh list of active maps & their instances
Copy link
Member

Choose a reason for hiding this comment

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

The comment should be kept (the new comment is also fine, you should have both)

}

//Refresh list of active maps & their instances
foreach (var (instanceId, mapInstance) in ActiveMapInstances.ToArray())
Copy link
Member

Choose a reason for hiding this comment

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

The only thing is that the new condition isn't safe against race conditions, even if this thread isn't changing the collection, another thread can. Is the collection only editable by this thread?

ActiveMapInstances.Remove(id);
}

// Allow our instance controllers to keep their instances up to date
Copy link
Member

Choose a reason for hiding this comment

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

Should have both comments

Comment on lines +200 to +207
var activeCount = ActiveMapInstances.Count;
if (_activeMapInstancesBuffer.Length != activeCount)
{
_activeMapInstancesBuffer = new MapInstance[activeCount];
}

ActiveMapInstances.Values.CopyTo(_activeMapInstancesBuffer, 0);
InstanceProcessor.UpdateInstanceControllers(_activeMapInstancesBuffer);
Copy link
Member

Choose a reason for hiding this comment

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

Better way of doing this is to change UpdateInstanceControllers() to take in a second default paramter int count = -1, and if count is -1 then change it to the length of the passed in array.

If count is 0 or positive, then loop through only those elements.

Why?

So then you only reallocate when the new count is larger (or significantly smaller).

Larger is simple, if it needs even 1 more you resize, but maybe resize with a buffer of 10%.

Significantly smaller can be defined as 45% or less of the current size (you still want a 10% buffer, that would get to around 50% of the current length taking that into account) but it also has to be a difference of at least 128 elements before you will shrink (if you go too low then it will constantly reallocate when you only have a few maps, and the cost of the array isn't high).

The only caveat is that you also need to clear "unused" elements in the array to null so that you don't make some elements live past their lifetime.

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