Skip to main content

Performance Issue

Summary of findings


Why 220 MB?

Rough breakdown:

  • .NET runtime/JIT/assemblies: ~50-80MB (baseline, unavoidable)
  • Garbage Collection pressure from .ToList() in hot-path timers: ~30-50MB fragmentation.
  • EF Core / DB connection pooling: ~10-20MB.
  • Framework libraries & buffers: ~40-60MB
  • Application data (players, UI, caches, track data): ~5-10MB

The .NET runtime itself accounts for ~100MB+ baseline. The rest is largely GC pressure from allocation patterns.


Top Recommendations

Remove .ToList() from timer hot paths (biggest win)

Your MillisecondTimer runs every 50ms and calls .ToList() on players, views, and buttons each iteration. That's thousands of throwaway list allocations per minute.

Files:

  • Timer/MillisecondTimer.cs (lines 54, 103, 110).
  • Timer/SecondTimer.cs (lines 110, 907, 1044+).


// BEFORE - allocates a new list every 50ms

foreach (var conn in _storage.Players.Values.ToList())


// AFTER - iterates in-place, zero allocation

foreach (var conn in _storage.Players.Values)

ConcurrentDictionary.Values is safe to enumerate even if modified during iteration - it provides a snapshot enumerator. If you're worried about collection-modified exceptions on the inner Views/Buttons lists, consider using a lock or switching

them to concurrent collections instead of .ToList() on every tick.

Expected savings: 20-50 MB reduction in GC pressure/fragmentation.

2. Audit all .ToList() across the codebase

The explore found .ToList() heavily used in InterfaceManager.Menu.cs, UserAction.cs, and other files. Many of these may be unnecessary. Each one allocates a copy of the entire collection.

3. Fix unbounded PendingOnTrack dictionary

In InSim/Packet/JoinTrack.cs line 27, entries are added but never cleaned up. Over weeks of uptime this slowly leaks. Add a cleanup mechanism similar to NewConnectionInfo.

4. Audit User.TunnelHeight list

In Model/User.cs line 196, List<double> TunnelHeight grows while TunnelHeightActive is true with no cap. If a player stays in a tunnel area for a long time, this list grows unbounded. Add a max size or clear it periodically.

5. Use Random.Shared instead of new Random()

Found 12 instances of new Random() scattered across the codebase (e.g., LocationManager.cs line 286). Each creates an unnecessary object. Use the static Random.Shared instead:

// BEFORE

var jobRange = new Random().Next(5, 10);

// AFTER

var jobRange = Random.Shared.Next(5, 10);

6. Consider trimming the GC on idle

You can add this to Program.cs to let .NET return memory to the OS more aggressively:

GCSettings.LatencyMode = GCLatencyMode.SustainedLowLatency;

Or periodically call GC.Collect(2, GCCollectionMode.Optimized) during low-activity periods.


What's NOT a problem

- Player cleanup on disconnect is correct (ConnectionLeave.cs removes from dictionary).

- DbContext usage with IDbContextFactory and await using is proper.

- Track data in memory (~300KB) is negligible.

- ModCache is small.


The single biggest win is removing .ToList() from the timer loops. That alone should noticeably reduce memory usage and GC pressure. Want me to make any of these changes?