diff --git a/managed/src/SwiftlyS2.Core/Modules/Menus/MenuAPI.cs b/managed/src/SwiftlyS2.Core/Modules/Menus/MenuAPI.cs index d77292b44..18d628202 100644 --- a/managed/src/SwiftlyS2.Core/Modules/Menus/MenuAPI.cs +++ b/managed/src/SwiftlyS2.Core/Modules/Menus/MenuAPI.cs @@ -106,7 +106,7 @@ public IReadOnlyList Options { // public event EventHandler? OptionLeaving; private readonly ISwiftlyCore core; - private readonly List options = new(); + private readonly List options = []; private readonly Lock optionsLock = new(); // Lock for synchronizing modifications to the `options` private readonly ConcurrentDictionary selectedOptionIndex = new(); // Stores the currently selected option index for each player // NOTE: Menu selection movement is entirely driven by changes to `desiredOptionIndex` (independent of any other variables) @@ -118,9 +118,9 @@ public IReadOnlyList Options { private readonly ConcurrentDictionary autoCloseCancelTokens = new(); // private readonly ConcurrentDictionary renderCache = new(); - private readonly CancellationTokenSource renderLoopCancellationTokenSource = new(); + private readonly ConcurrentDictionary renderLoopTasks = new(); private readonly Lock viewersLock = new(); - private readonly HashSet viewers = new(); + private readonly HashSet viewers = []; private volatile bool disposed; @@ -154,27 +154,6 @@ public MenuAPI( ISwiftlyCore core, MenuConfiguration configuration, MenuKeybindO // maxDisplayLines = 0; // core.Event.OnTick += OnTick; - - _ = Task.Run(async () => - { - var token = renderLoopCancellationTokenSource.Token; - var delayMilliseconds = (int)(1000f / 64f / 2f); - while (!token.IsCancellationRequested || disposed) - { - try - { - OnRender(); - await Task.Delay(delayMilliseconds, token); - } - catch (OperationCanceledException) - { - break; - } - catch - { - } - } - }, renderLoopCancellationTokenSource.Token); } ~MenuAPI() @@ -222,8 +201,15 @@ public void Dispose() // core.Event.OnTick -= OnTick; - renderLoopCancellationTokenSource?.Cancel(); - renderLoopCancellationTokenSource?.Dispose(); + renderLoopTasks.Keys.ToList().ForEach(task => + { + if (renderLoopTasks.TryRemove(task, out var cts)) + { + cts.Cancel(); + cts.Dispose(); + } + }); + renderLoopTasks.Clear(); disposed = true; GC.SuppressFinalize(this); @@ -478,6 +464,38 @@ public void ShowForPlayer( IPlayer player ) if (viewers.Count == 1) { + renderLoopTasks.Keys.ToList().ForEach(task => + { + if (renderLoopTasks.TryRemove(task, out var cts)) + { + cts.Cancel(); + cts.Dispose(); + } + }); + + var cts = new CancellationTokenSource(); + var token = cts.Token; + var delayMilliseconds = (int)(1000f / 64f); + var task = Task.Run(async () => + { + while (!token.IsCancellationRequested && !disposed) + { + try + { + OnRender(); + await Task.Delay(delayMilliseconds, token); + } + catch (OperationCanceledException) + { + break; + } + catch + { + } + } + }, token); + _ = renderLoopTasks.TryAdd(task, cts); + lock (optionsLock) { options.OfType().ToList().ForEach(option => option.ResumeTextAnimation()); @@ -526,6 +544,15 @@ public void HideForPlayer( IPlayer player ) if (viewers.Count == 0) { + renderLoopTasks.Keys.ToList().ForEach(task => + { + if (renderLoopTasks.TryRemove(task, out var cts)) + { + cts.Cancel(); + cts.Dispose(); + } + }); + lock (optionsLock) { options.OfType().ToList().ForEach(option => option.PauseTextAnimation()); diff --git a/managed/src/SwiftlyS2.Core/Modules/Menus/OptionsBase/Helpers/TextStyleProcessor.cs b/managed/src/SwiftlyS2.Core/Modules/Menus/OptionsBase/Helpers/TextStyleProcessor.cs index ec3a53ff4..8baa2f41c 100644 --- a/managed/src/SwiftlyS2.Core/Modules/Menus/OptionsBase/Helpers/TextStyleProcessor.cs +++ b/managed/src/SwiftlyS2.Core/Modules/Menus/OptionsBase/Helpers/TextStyleProcessor.cs @@ -9,6 +9,7 @@ namespace SwiftlyS2.Core.Menus.OptionsBase.Helpers; internal sealed partial class TextStyleProcessor : IDisposable { private readonly ConcurrentDictionary scrollOffsets = new(); + private readonly ConcurrentDictionary staticStyleCache = new(); private volatile bool disposed; @@ -16,6 +17,7 @@ public TextStyleProcessor() { disposed = false; scrollOffsets.Clear(); + staticStyleCache.Clear(); } ~TextStyleProcessor() @@ -32,6 +34,7 @@ public void Dispose() // Console.WriteLine($"{GetType().Name} has been disposed."); scrollOffsets.Clear(); + staticStyleCache.Clear(); disposed = true; GC.SuppressFinalize(this); @@ -42,19 +45,45 @@ public void Dispose() public (string styledText, int scrollOffset) ApplyHorizontalStyle( string text, MenuOptionTextStyle textStyle, float maxWidth ) { - return string.IsNullOrWhiteSpace(text) - ? (text, -1) - : Helper.EstimateTextWidth(StripHtmlTags(text)) <= maxWidth - ? (text, -1) - : textStyle switch { - MenuOptionTextStyle.TruncateEnd => TruncateTextEnd(text, maxWidth), - MenuOptionTextStyle.TruncateBothEnds => TruncateTextBothEnds(text, maxWidth), - MenuOptionTextStyle.ScrollLeftFade => ScrollTextWithFade(text, maxWidth, true), - MenuOptionTextStyle.ScrollRightFade => ScrollTextWithFade(text, maxWidth, false), - MenuOptionTextStyle.ScrollLeftLoop => ScrollTextWithLoop($"{text.TrimEnd()} ", maxWidth, true), - MenuOptionTextStyle.ScrollRightLoop => ScrollTextWithLoop($" {text.TrimStart()}", maxWidth, false), - _ => (text, -1) - }; + if (string.IsNullOrWhiteSpace(text)) + { + return (text, -1); + } + + if (Helper.EstimateTextWidth(StripHtmlTags(text)) <= maxWidth) + { + return (text, -1); + } + + if (textStyle == MenuOptionTextStyle.TruncateEnd || textStyle == MenuOptionTextStyle.TruncateBothEnds) + { + // Cache static styles (TruncateEnd and TruncateBothEnds) + var cacheKey = $"{text}_{textStyle}_{maxWidth}"; + if (staticStyleCache.TryGetValue(cacheKey, out var cachedStyledText)) + { + return (cachedStyledText, -1); + } + + var (styledText, scrollOffset) = textStyle switch { + MenuOptionTextStyle.TruncateEnd => TruncateTextEnd(text, maxWidth), + MenuOptionTextStyle.TruncateBothEnds => TruncateTextBothEnds(text, maxWidth), + _ => (text, -1) + }; + + _ = staticStyleCache.TryAdd(cacheKey, styledText); + return (styledText, scrollOffset); + } + else + { + // Dynamic styles (scrolling animations) + return textStyle switch { + MenuOptionTextStyle.ScrollLeftFade => ScrollTextWithFade(text, maxWidth, true), + MenuOptionTextStyle.ScrollRightFade => ScrollTextWithFade(text, maxWidth, false), + MenuOptionTextStyle.ScrollLeftLoop => ScrollTextWithLoop($"{text.TrimEnd()} ", maxWidth, true), + MenuOptionTextStyle.ScrollRightLoop => ScrollTextWithLoop($" {text.TrimStart()}", maxWidth, false), + _ => (text, -1) + }; + } } private (string styledText, int scrollOffset) ScrollTextWithFade( string text, float maxWidth, bool scrollLeft ) diff --git a/managed/src/SwiftlyS2.Core/Services/CoreCommandService.cs b/managed/src/SwiftlyS2.Core/Services/CoreCommandService.cs index a6d9373cd..46fb0e20d 100644 --- a/managed/src/SwiftlyS2.Core/Services/CoreCommandService.cs +++ b/managed/src/SwiftlyS2.Core/Services/CoreCommandService.cs @@ -42,12 +42,20 @@ void ShowPlayerList() void ShowServerStatus() { var uptime = DateTime.Now - System.Diagnostics.Process.GetCurrentProcess().StartTime; + ThreadPool.GetAvailableThreads(out var availableWorkerThreads, out var availableCompletionPortThreads); + ThreadPool.GetMaxThreads(out var maxWorkerThreads, out var maxCompletionPortThreads); + var busyWorkerThreads = maxWorkerThreads - availableWorkerThreads; + var processThreadCount = System.Diagnostics.Process.GetCurrentProcess().Threads.Count; + var output = string.Join("\n", [ $"Uptime: {uptime.Days}d {uptime.Hours}h {uptime.Minutes}m {uptime.Seconds}s", $"Managed Heap Memory: {GC.GetTotalMemory(false) / 1024.0f / 1024.0f:0.00} MB", + $"Process Threads: {processThreadCount}", + $"ThreadPool Worker Threads: {busyWorkerThreads}/{maxWorkerThreads} (Busy/Max)", + $"ThreadPool Completion Port Threads: {maxCompletionPortThreads - availableCompletionPortThreads}/{maxCompletionPortThreads} (Busy/Max)", $"Loaded Plugins: {pluginManager.GetPlugins().Count}", $"Players: {core.PlayerManager.PlayerCount}/{core.Engine.GlobalVars.MaxClients}", - $"Map: {core.Engine.GlobalVars.MapName.Value}" + $"Map: {core.Engine.GlobalVars.MapName.Value}", ]); logger.LogInformation("{Output}", output); }