From be086cf0056294bc5643bde3bb5b7b73aa4b362f Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 22 Jun 2022 01:53:37 -0400 Subject: [PATCH] avoid unneeded overhead of ConcurrentQueue --- docs/release-notes.md | 3 ++ src/SMAPI/Framework/CommandQueue.cs | 47 +++++++++++++++++++++++++++++ src/SMAPI/Framework/SCore.cs | 43 +++++++++++++------------- 3 files changed, 72 insertions(+), 21 deletions(-) create mode 100644 src/SMAPI/Framework/CommandQueue.cs diff --git a/docs/release-notes.md b/docs/release-notes.md index 16b47670..622a146a 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -8,6 +8,9 @@ --> ## Upcoming release +* For players: + * Minor optimizations. + * For the web UI: * Fixed the mod count in the log parser metadata. diff --git a/src/SMAPI/Framework/CommandQueue.cs b/src/SMAPI/Framework/CommandQueue.cs new file mode 100644 index 00000000..c51016ad --- /dev/null +++ b/src/SMAPI/Framework/CommandQueue.cs @@ -0,0 +1,47 @@ +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace StardewModdingAPI.Framework +{ + /// A thread-safe command queue optimized for infrequent changes. + internal class CommandQueue + { + /******** + ** Fields + ********/ + /// The underlying list of queued commands to parse and execute. + private readonly List RawCommandQueue = new(); + + + /******** + ** Public methods + ********/ + /// Add a command to the queue. + /// The command to add. + public void Add(string command) + { + lock (this.RawCommandQueue) + this.RawCommandQueue.Add(command); + } + + /// Remove and return all queued commands, if any. + /// The commands that were dequeued, in the order they were originally queued. + /// Returns whether any values were dequeued. + [SuppressMessage("ReSharper", "InconsistentlySynchronizedField", Justification = "Deliberately check if it's empty before locking unnecessarily.")] + public bool TryDequeue([NotNullWhen(true)] out string[]? queued) + { + if (this.RawCommandQueue.Count is 0) + { + queued = null; + return false; + } + + lock (this.RawCommandQueue) + { + queued = this.RawCommandQueue.ToArray(); + this.RawCommandQueue.Clear(); + return queued.Length > 0; + } + } + } +} diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index ff3eadf5..cc332225 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; @@ -147,8 +146,7 @@ namespace StardewModdingAPI.Framework #endif /// A list of queued commands to parse and execute. - /// This property must be thread-safe, since it's accessed from a separate console input thread. - private readonly ConcurrentQueue RawCommandQueue = new(); + private readonly CommandQueue RawCommandQueue = new(); /// A list of commands to execute on each screen. private readonly PerScreen> ScreenCommandQueue = new(() => new List()); @@ -437,7 +435,7 @@ namespace StardewModdingAPI.Framework () => this.LogManager.RunConsoleInputLoop( commandManager: this.CommandManager, reloadTranslations: this.ReloadTranslations, - handleInput: input => this.RawCommandQueue.Enqueue(input), + handleInput: input => this.RawCommandQueue.Add(input), continueWhile: () => this.IsGameRunning && !this.CancellationToken.IsCancellationRequested ) ).Start(); @@ -525,29 +523,32 @@ namespace StardewModdingAPI.Framework /********* ** Parse commands *********/ - while (this.RawCommandQueue.TryDequeue(out string? rawInput)) + if (this.RawCommandQueue.TryDequeue(out string[]? rawCommands)) { - // parse command - string? name; - string[]? args; - Command? command; - int screenId; - try + foreach (string rawInput in rawCommands) { - if (!this.CommandManager.TryParse(rawInput, out name, out args, out command, out screenId)) + // parse command + string? name; + string[]? args; + Command? command; + int screenId; + try { - this.Monitor.Log("Unknown command; type 'help' for a list of available commands.", LogLevel.Error); + if (!this.CommandManager.TryParse(rawInput, out name, out args, out command, out screenId)) + { + this.Monitor.Log("Unknown command; type 'help' for a list of available commands.", LogLevel.Error); + continue; + } + } + catch (Exception ex) + { + this.Monitor.Log($"Failed parsing that command:\n{ex.GetLogSummary()}", LogLevel.Error); continue; } - } - catch (Exception ex) - { - this.Monitor.Log($"Failed parsing that command:\n{ex.GetLogSummary()}", LogLevel.Error); - continue; - } - // queue command for screen - this.ScreenCommandQueue.GetValueForScreen(screenId).Add(new(command, name, args)); + // queue command for screen + this.ScreenCommandQueue.GetValueForScreen(screenId).Add(new(command, name, args)); + } }