From 6c5564163346bf4ee81ae307b17606c088d70a4b Mon Sep 17 00:00:00 2001 From: Arufonsu <17498701+Arufonsu@users.noreply.github.com> Date: Sat, 21 Feb 2026 21:33:54 -0300 Subject: [PATCH] performance: reduce LINQ allocations in UpdateInstanceControllers Replace LINQ-heavy hot paths in InstanceProcessor that were executing every tick with zero-allocation equivalents. Changes: - Add a static `_activeIdScratch` HashSet reused across calls to CleanupOrphanedControllers, replacing the LINQ .Except().ToArray() chain that allocated on every invocation. - Change CleanupOrphanedControllers signature from IEnumerable to MapInstance[] so the caller's already-available array is used directly, avoiding the intermediate .Select(map => map.MapInstanceId) projection. - Replaces GroupBy().ToDictionary().ToArray() in UpdateInstanceControllers with a manual Dictionary> build loop, removing three allocating LINQ operators from the 250 ms update path. - Use lazy-init (toRemove ??= new List()) so the removal list is only allocated when at least one orphaned controller is actually found. - Mark InstanceControllers as readonly to prevent accidental reassignment. No behaviour changes; purely allocation and GC pressure reduction. Signed-off-by: Arufonsu <17498701+Arufonsu@users.noreply.github.com> --- .../Core/MapInstancing/InstanceProcessor.cs | 63 +++++++++++++------ 1 file changed, 43 insertions(+), 20 deletions(-) diff --git a/Intersect.Server.Core/Core/MapInstancing/InstanceProcessor.cs b/Intersect.Server.Core/Core/MapInstancing/InstanceProcessor.cs index d1f9060e34..1d2c48bfdb 100644 --- a/Intersect.Server.Core/Core/MapInstancing/InstanceProcessor.cs +++ b/Intersect.Server.Core/Core/MapInstancing/InstanceProcessor.cs @@ -5,28 +5,44 @@ using Microsoft.Extensions.Logging; namespace Intersect.Server.Core.MapInstancing; + public static class InstanceProcessor { - private static Dictionary InstanceControllers = new(); + private static readonly Dictionary InstanceControllers = new(); - public static Guid[] CurrentControllers => InstanceControllers.Keys.ToArray(); + private static readonly HashSet ActiveIdScratch = []; - public static bool TryGetInstanceController(Guid instanceId, out InstanceController controller) - { - return InstanceControllers.TryGetValue(instanceId, out controller); - } + public static bool TryGetInstanceController(Guid instanceId, out InstanceController controller) => InstanceControllers.TryGetValue(instanceId, out controller); - private static void CleanupOrphanedControllers(IEnumerable activeInstanceIds) + private static void CleanupOrphanedControllers(MapInstance[] activeMaps) { - var processingInstances = InstanceControllers.Keys - .Except(activeInstanceIds) - .Except(new Guid[1] { default }) // Never cleanup the overworld instance - .ToArray(); + ActiveIdScratch.Clear(); + for (var i = 0; i < activeMaps.Length; i++) + { + ActiveIdScratch.Add(activeMaps[i].MapInstanceId); + } + + List? toRemove = null; + foreach (var id in InstanceControllers.Keys) + { + if (id == default) + { + continue; // never clean overworld + } + + if (!ActiveIdScratch.Contains(id)) + { + (toRemove ??= new List()).Add(id); + } + } - foreach (var id in processingInstances) + if (toRemove != null) { - InstanceControllers.Remove(id); - ApplicationContext.Context.Value?.Logger.LogDebug($"Removing instance controller {id}"); + foreach (var id in toRemove) + { + InstanceControllers.Remove(id); + ApplicationContext.Context.Value?.Logger.LogDebug($"Removing instance controller {id}"); + } } } @@ -48,14 +64,21 @@ public static void UpdateInstanceControllers(MapInstance[] activeMaps) return; } - // Cleanup inactive instances - CleanupOrphanedControllers(activeMaps.Select(map => map.MapInstanceId)); + CleanupOrphanedControllers(activeMaps); - Dictionary mapsAndInstances = activeMaps - .GroupBy(m => m.MapInstanceId) - .ToDictionary(m => m.Key, m => m.ToArray()); + // Manual grouping + ToDictionary allocations + var mapsAndInstances = new Dictionary>(); + for (var i = 0; i < activeMaps.Length; i++) + { + var map = activeMaps[i]; + if (!mapsAndInstances.TryGetValue(map.MapInstanceId, out var list)) + { + mapsAndInstances[map.MapInstanceId] = list = new List(); + } + + list.Add(map); + } - // For each instance... foreach (var (instanceId, mapsInInstance) in mapsAndInstances) { // Fetch our instance controller...