From a751252c4ee3b48977d5d24c36a4e4e5466f93db Mon Sep 17 00:00:00 2001 From: Drachenkaetzchen Date: Fri, 10 Jan 2020 01:27:56 +0100 Subject: Initial commit of the performance counters --- .../Framework/Commands/Other/PerformanceCounterCommand.cs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/PerformanceCounterCommand.cs (limited to 'src/SMAPI.Mods.ConsoleCommands/Framework') diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/PerformanceCounterCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/PerformanceCounterCommand.cs new file mode 100644 index 00000000..b7e56359 --- /dev/null +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/PerformanceCounterCommand.cs @@ -0,0 +1,14 @@ +namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other +{ + internal class PerformanceCounterCommand: TrainerCommand + { + public PerformanceCounterCommand(string name, string description) : base("performance_counters", "Displays performance counters") + { + } + + public override void Handle(IMonitor monitor, string command, ArgumentParser args) + { + + } + } +} -- cgit From 280dc911839f8996cddd9804f3f545cc38d20243 Mon Sep 17 00:00:00 2001 From: Drachenkaetzchen Date: Sat, 11 Jan 2020 15:45:45 +0100 Subject: Reworked the console implementation, added monitoring. Some internal refactoring. --- .../Framework/Commands/ArgumentParser.cs | 63 +++ .../Commands/Other/PerformanceCounterCommand.cs | 533 ++++++++++++++++++++- .../SMAPI.Mods.ConsoleCommands.csproj | 4 + src/SMAPI/Constants.cs | 2 +- src/SMAPI/Framework/Events/EventManager.cs | 5 +- src/SMAPI/Framework/Events/IManagedEvent.cs | 7 + src/SMAPI/Framework/Events/ManagedEvent.cs | 103 ++-- .../Framework/PerformanceCounter/AlertContext.cs | 14 + .../Framework/PerformanceCounter/AlertEntry.cs | 20 + .../EventPerformanceCounterCategory.cs | 16 - .../EventPerformanceCounterCollection.cs | 11 + .../PerformanceCounter/IPerformanceCounterEvent.cs | 1 - .../PerformanceCounter/PerformanceCounter.cs | 18 +- .../PerformanceCounterCollection.cs | 144 ++++++ .../PerformanceCounterManager.cs | 233 ++++++--- src/SMAPI/Framework/SCore.cs | 193 +------- src/SMAPI/Framework/SGame.cs | 10 +- src/SMAPI/Program.cs | 2 +- 18 files changed, 1027 insertions(+), 352 deletions(-) create mode 100644 src/SMAPI/Framework/Events/IManagedEvent.cs create mode 100644 src/SMAPI/Framework/PerformanceCounter/AlertContext.cs create mode 100644 src/SMAPI/Framework/PerformanceCounter/AlertEntry.cs delete mode 100644 src/SMAPI/Framework/PerformanceCounter/EventPerformanceCounterCategory.cs create mode 100644 src/SMAPI/Framework/PerformanceCounter/EventPerformanceCounterCollection.cs create mode 100644 src/SMAPI/Framework/PerformanceCounter/PerformanceCounterCollection.cs (limited to 'src/SMAPI.Mods.ConsoleCommands/Framework') diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/ArgumentParser.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/ArgumentParser.cs index 10007b42..40691a3e 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/ArgumentParser.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/ArgumentParser.cs @@ -1,6 +1,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Globalization; using System.Linq; namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands @@ -113,6 +114,51 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands return true; } + public bool IsDecimal(int index) + { + if (!this.TryGet(index, "", out string raw, false)) + return false; + + if (!decimal.TryParse(raw, NumberStyles.Number, CultureInfo.InvariantCulture, out decimal value)) + { + return false; + } + + return true; + } + + /// Try to read a decimal argument. + /// The argument index. + /// The argument name for error messages. + /// The parsed value. + /// Whether to show an error if the argument is missing. + /// The minimum value allowed. + /// The maximum value allowed. + public bool TryGetDecimal(int index, string name, out decimal value, bool required = true, decimal? min = null, decimal? max = null) + { + value = 0; + + // get argument + if (!this.TryGet(index, name, out string raw, required)) + return false; + + // parse + if (!decimal.TryParse(raw, NumberStyles.Number, CultureInfo.InvariantCulture, out value)) + { + this.LogDecimalFormatError(index, name, min, max); + return false; + } + + // validate + if ((min.HasValue && value < min) || (max.HasValue && value > max)) + { + this.LogDecimalFormatError(index, name, min, max); + return false; + } + + return true; + } + /// Returns an enumerator that iterates through the collection. /// An enumerator that can be used to iterate through the collection. public IEnumerator GetEnumerator() @@ -154,5 +200,22 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands else this.LogError($"Argument {index} ({name}) must be an integer."); } + + /// Print an error for an invalid decimal argument. + /// The argument index. + /// The argument name for error messages. + /// The minimum value allowed. + /// The maximum value allowed. + private void LogDecimalFormatError(int index, string name, decimal? min, decimal? max) + { + if (min.HasValue && max.HasValue) + this.LogError($"Argument {index} ({name}) must be a decimal between {min} and {max}."); + else if (min.HasValue) + this.LogError($"Argument {index} ({name}) must be a decimal and at least {min}."); + else if (max.HasValue) + this.LogError($"Argument {index} ({name}) must be a decimal and at most {max}."); + else + this.LogError($"Argument {index} ({name}) must be a decimal."); + } } } diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/PerformanceCounterCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/PerformanceCounterCommand.cs index b7e56359..84b9504e 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/PerformanceCounterCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/PerformanceCounterCommand.cs @@ -1,14 +1,543 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using StardewModdingAPI.Framework; +using StardewModdingAPI.Framework.PerformanceCounter; + namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other { - internal class PerformanceCounterCommand: TrainerCommand + internal class PerformanceCounterCommand : TrainerCommand { - public PerformanceCounterCommand(string name, string description) : base("performance_counters", "Displays performance counters") + private readonly Dictionary CommandNames = new Dictionary() + { + {Command.Summary, new[] {"summary", "sum", "s"}}, + {Command.Detail, new[] {"detail", "d"}}, + {Command.Reset, new[] {"reset", "r"}}, + {Command.Monitor, new[] {"monitor"}}, + {Command.Examples, new[] {"examples"}}, + {Command.Concepts, new[] {"concepts"}}, + {Command.Help, new[] {"help"}}, + }; + + private enum Command + { + Summary, + Detail, + Reset, + Monitor, + Examples, + Help, + Concepts, + None + } + + public PerformanceCounterCommand() : base("pc", PerformanceCounterCommand.GetDescription()) { } public override void Handle(IMonitor monitor, string command, ArgumentParser args) { + if (args.TryGet(0, "command", out string subCommandString, false)) + { + Command subCommand = this.ParseCommandString(subCommandString); + + switch (subCommand) + { + case Command.Summary: + this.DisplayPerformanceCounterSummary(monitor, args); + break; + case Command.Detail: + this.DisplayPerformanceCounterDetail(monitor, args); + break; + case Command.Reset: + this.ResetCounter(monitor, args); + break; + case Command.Monitor: + this.HandleMonitor(monitor, args); + break; + case Command.Examples: + break; + case Command.Concepts: + this.ShowHelp(monitor, Command.Concepts); + break; + case Command.Help: + args.TryGet(1, "command", out string commandString, true); + + var helpCommand = this.ParseCommandString(commandString); + this.ShowHelp(monitor, helpCommand); + break; + default: + this.LogUsageError(monitor, $"Unknown command {subCommandString}"); + break; + } + } + else + { + this.DisplayPerformanceCounterSummary(monitor, args); + } + } + + private Command ParseCommandString(string command) + { + foreach (var i in this.CommandNames.Where(i => i.Value.Any(str => str.Equals(command, StringComparison.InvariantCultureIgnoreCase)))) + { + return i.Key; + } + + return Command.None; + } + + private void HandleMonitor(IMonitor monitor, ArgumentParser args) + { + if (args.TryGet(1, "mode", out string mode, false)) + { + switch (mode) + { + case "list": + this.ListMonitors(monitor); + break; + case "collection": + args.TryGet(2, "name", out string collectionName); + decimal threshold = 0; + if (args.IsDecimal(3) && args.TryGetDecimal(3, "threshold", out threshold, false)) + { + this.SetCollectionMonitor(monitor, collectionName, null, (double)threshold); + } else if (args.TryGet(3, "source", out string source)) + { + if (args.TryGetDecimal(4, "threshold", out threshold)) + { + this.SetCollectionMonitor(monitor, collectionName, source, (double) threshold); + } + } + break; + case "clear": + this.ClearMonitors(monitor); + break; + default: + monitor.Log($"Unknown mode {mode}. See 'pc help monitor' for usage."); + break; + } + + } + else + { + this.ListMonitors(monitor); + } + } + + private void SetCollectionMonitor(IMonitor monitor, string collectionName, string sourceName, double threshold) + { + foreach (PerformanceCounterCollection collection in SCore.PerformanceCounterManager.PerformanceCounterCollections) + { + if (collection.Name.ToLowerInvariant().Equals(collectionName.ToLowerInvariant())) + { + if (sourceName == null) + { + collection.Monitor = true; + collection.MonitorThresholdMilliseconds = threshold; + monitor.Log($"Set up monitor for '{collectionName}' with '{this.FormatMilliseconds(threshold)}'", LogLevel.Info); + return; + } + else + { + foreach (var performanceCounter in collection.PerformanceCounters) + { + if (performanceCounter.Value.Source.ToLowerInvariant().Equals(sourceName.ToLowerInvariant())) + { + performanceCounter.Value.Monitor = true; + performanceCounter.Value.MonitorThresholdMilliseconds = threshold; + monitor.Log($"Set up monitor for '{sourceName}' in collection '{collectionName}' with '{this.FormatMilliseconds(threshold)}", LogLevel.Info); + return; + } + } + + monitor.Log($"Could not find the source '{sourceName}' in collection '{collectionName}'", LogLevel.Warn); + return; + } + } + } + + monitor.Log($"Could not find the collection '{collectionName}'", LogLevel.Warn); + } + + + private void ClearMonitors(IMonitor monitor) + { + int clearedCounters = 0; + foreach (PerformanceCounterCollection collection in SCore.PerformanceCounterManager.PerformanceCounterCollections) + { + if (collection.Monitor) + { + collection.Monitor = false; + clearedCounters++; + } + + foreach (var performanceCounter in collection.PerformanceCounters) + { + if (performanceCounter.Value.Monitor) + { + performanceCounter.Value.Monitor = false; + clearedCounters++; + } + } + + } + + monitor.Log($"Cleared {clearedCounters} counters.", LogLevel.Info); + } + + private void ListMonitors(IMonitor monitor) + { + StringBuilder sb = new StringBuilder(); + sb.AppendLine(); + sb.AppendLine(); + var collectionMonitors = new List<(string collectionName, double threshold)>(); + var sourceMonitors = new List<(string collectionName, string sourceName, double threshold)>(); + + foreach (PerformanceCounterCollection collection in SCore.PerformanceCounterManager.PerformanceCounterCollections) + { + if (collection.Monitor) + { + collectionMonitors.Add((collection.Name, collection.MonitorThresholdMilliseconds)); + } + + sourceMonitors.AddRange(from performanceCounter in + collection.PerformanceCounters where performanceCounter.Value.Monitor + select (collection.Name, performanceCounter.Value.Source, performanceCounter.Value.MonitorThresholdMilliseconds)); + } + + if (collectionMonitors.Count > 0) + { + sb.AppendLine("Collection Monitors:"); + sb.AppendLine(); + sb.AppendLine(this.GetTableString( + data: collectionMonitors, + header: new[] {"Collection", "Threshold"}, + getRow: item => new[] + { + item.collectionName, + this.FormatMilliseconds(item.threshold) + } + )); + + sb.AppendLine(); + + + } + + if (sourceMonitors.Count > 0) + { + sb.AppendLine("Source Monitors:"); + sb.AppendLine(); + sb.AppendLine(this.GetTableString( + data: sourceMonitors, + header: new[] {"Collection", "Source", "Threshold"}, + getRow: item => new[] + { + item.collectionName, + item.sourceName, + this.FormatMilliseconds(item.threshold) + } + )); + + sb.AppendLine(); + } + + monitor.Log(sb.ToString(), LogLevel.Info); + } + + private void ShowHelp(IMonitor monitor, Command command) + { + StringBuilder sb = new StringBuilder(); + sb.AppendLine(); + switch (command) + { + case Command.Concepts: + sb.AppendLine("A performance counter is a metric which measures execution time. Each performance"); + sb.AppendLine("counter consists of:"); + sb.AppendLine(); + sb.AppendLine(" - A source, which typically is a mod or the game itself."); + sb.AppendLine(" - A ring buffer which stores the data points (execution time and time when it was executed)"); + sb.AppendLine(); + sb.AppendLine("A set of performance counters is organized in a collection to group various areas."); + sb.AppendLine("Per default, collections for all game events [1] are created."); + sb.AppendLine(); + sb.AppendLine("Example:"); + sb.AppendLine(); + sb.AppendLine("The performance counter collection named 'Display.Rendered' contains one performance"); + sb.AppendLine("counters when the game executes the 'Display.Rendered' event, and one additional"); + sb.AppendLine("performance counter for each mod which handles the 'Display.Rendered' event."); + sb.AppendLine(); + sb.AppendLine("[1] https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Events"); + break; + case Command.Detail: + sb.AppendLine("Usage: pc detail "); + sb.AppendLine(" pc detail "); + sb.AppendLine(); + sb.AppendLine("Displays details for a specific collection."); + sb.AppendLine(); + sb.AppendLine("Arguments:"); + sb.AppendLine(" Required. The full or partial name of the collection to display."); + sb.AppendLine(" Optional. The full or partial name of the source."); + sb.AppendLine(" Optional. The threshold in milliseconds. Any average execution time below that"); + sb.AppendLine(" threshold is not reported."); + sb.AppendLine(); + sb.AppendLine("Examples:"); + sb.AppendLine("pc detail Display.Rendering Displays all performance counters for the 'Display.Rendering' collection"); + sb.AppendLine("pc detail Display.Rendering Pathoschild.ChestsAnywhere Displays the 'Display.Rendering' performance counter for 'Pathoschild.ChestsAnywhere'"); + sb.AppendLine("pc detail Display.Rendering 5 Displays the 'Display.Rendering' performance counters exceeding an average of 5ms"); + break; + case Command.Summary: + sb.AppendLine("Usage: pc summary "); + sb.AppendLine(); + sb.AppendLine("Displays the performance counter summary."); + sb.AppendLine(); + sb.AppendLine("Arguments:"); + sb.AppendLine(" Optional. Defaults to 'important' if omitted. Specifies one of these modes:"); + sb.AppendLine(" - all Displays performance counters from all collections"); + sb.AppendLine(" - important Displays only important performance counter collections"); + sb.AppendLine(); + sb.AppendLine(" Optional. Only shows performance counter collections matching the given name"); + sb.AppendLine(); + sb.AppendLine("Examples:"); + sb.AppendLine("pc summary all Shows all events"); + sb.AppendLine("pc summary Display.Rendering Shows only the 'Display.Rendering' collection"); + break; + case Command.Monitor: + sb.AppendLine("Usage: pc monitor "); + sb.AppendLine("Usage: pc monitor "); + sb.AppendLine(); + sb.AppendLine("Manages monitoring settings."); + sb.AppendLine(); + sb.AppendLine("Arguments:"); + sb.AppendLine(" Optional. Specifies if a specific source or a specific collection should be monitored."); + sb.AppendLine(" - list Lists current monitoring settings"); + sb.AppendLine(" - collection Sets up a monitor for a collection"); + sb.AppendLine(" - clear Clears all monitoring entries"); + sb.AppendLine(" Defaults to 'list' if not specified."); + sb.AppendLine(); + sb.AppendLine(" Required if the mode 'collection' is specified."); + sb.AppendLine(" Specifies the name of the collection to be monitored. Must be an exact match."); + sb.AppendLine(); + sb.AppendLine(" Optional. Specifies the name of a specific source. Must be an exact match."); + sb.AppendLine(); + sb.AppendLine(" Required if the mode 'collection' is specified."); + sb.AppendLine(" Specifies the threshold in milliseconds (fractions allowed)."); + sb.AppendLine(" Can also be 'remove' to remove the threshold."); + sb.AppendLine(); + sb.AppendLine("Examples:"); + sb.AppendLine(); + sb.AppendLine("pc monitor collection Display.Rendering 10"); + sb.AppendLine(" Sets up monitoring to write an alert on the console if the execution time of all performance counters in"); + sb.AppendLine(" the 'Display.Rendering' collection exceed 10 milliseconds."); + sb.AppendLine(); + sb.AppendLine("pc monitor collection Display.Rendering Pathoschild.ChestsAnywhere 5"); + sb.AppendLine(" Sets up monitoring to write an alert on the console if the execution time of Pathoschild.ChestsAnywhere in"); + sb.AppendLine(" the 'Display.Rendering' collection exceed 5 milliseconds."); + sb.AppendLine(); + sb.AppendLine("pc monitor collection Display.Rendering remove"); + sb.AppendLine(" Removes the threshold previously defined from the collection. Note that source-specific thresholds are left intact."); + sb.AppendLine(); + sb.AppendLine("pc monitor clear"); + sb.AppendLine(" Clears all previously setup monitors."); + break; + case Command.Reset: + sb.AppendLine("Usage: pc reset "); + sb.AppendLine(); + sb.AppendLine("Resets performance counters."); + sb.AppendLine(); + sb.AppendLine("Arguments:"); + sb.AppendLine(" Optional. Specifies if a collection or source should be reset."); + sb.AppendLine(" If omitted, all performance counters are reset."); + sb.AppendLine(); + sb.AppendLine(" - source Clears performance counters for a specific source"); + sb.AppendLine(" - collection Clears performance counters for a specific collection"); + sb.AppendLine(); + sb.AppendLine(" Required if a is given. Specifies the name of either the collection"); + sb.AppendLine(" or the source. The name must be an exact match."); + sb.AppendLine(); + sb.AppendLine("Examples:"); + sb.AppendLine("pc reset Resets all performance counters"); + sb.AppendLine("pc reset source Pathoschild.ChestsAnywhere Resets all performance for the source named Pathoschild.ChestsAnywhere"); + sb.AppendLine("pc reset collection Display.Rendering Resets all performance for the collection named Display.Rendering"); + break; + } + + sb.AppendLine(); + monitor.Log(sb.ToString(), LogLevel.Info); + } + + private void ResetCounter(IMonitor monitor, ArgumentParser args) + { + if (args.TryGet(1, "type", out string type, false)) + { + args.TryGet(2, "name", out string name); + + switch (type) + { + case "category": + SCore.PerformanceCounterManager.ResetCategory(name); + monitor.Log($"All performance counters for category {name} are now cleared.", LogLevel.Info); + break; + case "mod": + SCore.PerformanceCounterManager.ResetSource(name); + monitor.Log($"All performance counters for mod {name} are now cleared.", LogLevel.Info); + break; + } + } + else + { + SCore.PerformanceCounterManager.Reset(); + monitor.Log("All performance counters are now cleared.", LogLevel.Info); + } + } + + private void DisplayPerformanceCounterSummary(IMonitor monitor, ArgumentParser args) + { + IEnumerable data; + + if (!args.TryGet(1, "mode", out string mode, false)) + { + mode = "important"; + } + + switch (mode) + { + case null: + case "important": + data = SCore.PerformanceCounterManager.PerformanceCounterCollections.Where(p => p.IsImportant); + break; + case "all": + data = SCore.PerformanceCounterManager.PerformanceCounterCollections; + break; + default: + data = SCore.PerformanceCounterManager.PerformanceCounterCollections.Where(p => + p.Name.ToLowerInvariant().Contains(mode.ToLowerInvariant())); + break; + } + + StringBuilder sb = new StringBuilder(); + + sb.AppendLine("Summary:"); + sb.AppendLine(this.GetTableString( + data: data, + header: new[] {"Collection", "Avg Calls/s", "Avg Execution Time (Game)", "Avg Execution Time (Mods)", "Avg Execution Time (Game+Mods)"}, + getRow: item => new[] + { + item.Name, + item.GetAverageCallsPerSecond().ToString(), + this.FormatMilliseconds(item.GetGameAverageExecutionTime()), + this.FormatMilliseconds(item.GetModsAverageExecutionTime()), + this.FormatMilliseconds(item.GetAverageExecutionTime()) + } + )); + + monitor.Log(sb.ToString(), LogLevel.Info); + } + + private void DisplayPerformanceCounterDetail(IMonitor monitor, ArgumentParser args) + { + List collections = new List(); + TimeSpan averageInterval = TimeSpan.FromSeconds(60); + double? thresholdMilliseconds = null; + string sourceFilter = null; + + if (args.TryGet(1, "collection", out string collectionName)) + { + collections.AddRange(SCore.PerformanceCounterManager.PerformanceCounterCollections.Where(collection => collection.Name.ToLowerInvariant().Contains(collectionName.ToLowerInvariant()))); + + if (args.IsDecimal(2) && args.TryGetDecimal(2, "threshold", out decimal value, false)) + { + thresholdMilliseconds = (double?) value; + } + else + { + if (args.TryGet(2, "source", out string sourceName, false)) + { + sourceFilter = sourceName; + } + } + } + + foreach (var c in collections) + { + this.DisplayPerformanceCollectionDetail(monitor, c, averageInterval, thresholdMilliseconds, sourceFilter); + } + } + + private void DisplayPerformanceCollectionDetail(IMonitor monitor, PerformanceCounterCollection collection, + TimeSpan averageInterval, double? thresholdMilliseconds, string sourceFilter = null) + { + StringBuilder sb = new StringBuilder($"Performance Counter for {collection.Name}:\n\n"); + + IEnumerable> data = collection.PerformanceCounters; + + if (sourceFilter != null) + { + data = collection.PerformanceCounters.Where(p => + p.Value.Source.ToLowerInvariant().Contains(sourceFilter.ToLowerInvariant())); + } + + if (thresholdMilliseconds != null) + { + data = data.Where(p => p.Value.GetAverage(averageInterval) >= thresholdMilliseconds); + } + + sb.AppendLine(this.GetTableString( + data: data, + header: new[] {"Mod", $"Avg Execution Time (last {(int) averageInterval.TotalSeconds}s)", "Last Execution Time", "Peak Execution Time"}, + getRow: item => new[] + { + item.Key, + this.FormatMilliseconds(item.Value.GetAverage(averageInterval), thresholdMilliseconds), + this.FormatMilliseconds(item.Value.GetLastEntry()?.Elapsed.TotalMilliseconds), + this.FormatMilliseconds(item.Value.GetPeak()?.Elapsed.TotalMilliseconds) + } + )); + + monitor.Log(sb.ToString(), LogLevel.Info); + } + + private string FormatMilliseconds(double? milliseconds, double? thresholdMilliseconds = null) + { + if (milliseconds == null || (thresholdMilliseconds != null && milliseconds < thresholdMilliseconds)) + { + return "-"; + } + + return ((double) milliseconds).ToString("F2"); + } + + /// Get the command description. + private static string GetDescription() + { + StringBuilder sb = new StringBuilder(); + + sb.AppendLine("Displays and configured performance counters."); + sb.AppendLine(); + sb.AppendLine("A performance counter records the invocation time of in-game events being"); + sb.AppendLine("processed by mods or the game itself. See 'concepts' for a detailed explanation."); + sb.AppendLine(); + sb.AppendLine("Usage: pc "); + sb.AppendLine(); + sb.AppendLine("Commands:"); + sb.AppendLine(); + sb.AppendLine(" summary|sum|s Displays a summary of important or all collections"); + sb.AppendLine(" detail|d Shows performance counter information for a given collection"); + sb.AppendLine(" reset|r Resets the performance counters"); + sb.AppendLine(" monitor Configures monitoring settings"); + sb.AppendLine(" examples Displays various examples"); + sb.AppendLine(" concepts Displays an explanation of the performance counter concepts"); + sb.AppendLine(" help Displays verbose help for the available commands"); + sb.AppendLine(); + sb.AppendLine("To get help for a specific command, use 'pc help ', for example:"); + sb.AppendLine("pc help summary"); + sb.AppendLine(); + sb.AppendLine("Defaults to summary if no command is given."); + sb.AppendLine(); + return sb.ToString(); } } } diff --git a/src/SMAPI.Mods.ConsoleCommands/SMAPI.Mods.ConsoleCommands.csproj b/src/SMAPI.Mods.ConsoleCommands/SMAPI.Mods.ConsoleCommands.csproj index ce35bf73..f073ac21 100644 --- a/src/SMAPI.Mods.ConsoleCommands/SMAPI.Mods.ConsoleCommands.csproj +++ b/src/SMAPI.Mods.ConsoleCommands/SMAPI.Mods.ConsoleCommands.csproj @@ -67,6 +67,10 @@ + + + + diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index 0923494c..76cb6f89 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -56,7 +56,7 @@ namespace StardewModdingAPI internal const string HomePageUrl = "https://smapi.io"; /// The URL of the SMAPI home page. - internal const string GamePerformanceCounterName = "-internal-"; + internal const string GamePerformanceCounterName = ""; /// The absolute path to the folder containing SMAPI's internal files. internal static readonly string InternalFilesPath = Program.DllSearchPath; diff --git a/src/SMAPI/Framework/Events/EventManager.cs b/src/SMAPI/Framework/Events/EventManager.cs index 892cbc7b..9c65a6cc 100644 --- a/src/SMAPI/Framework/Events/EventManager.cs +++ b/src/SMAPI/Framework/Events/EventManager.cs @@ -1,5 +1,6 @@ using System.Diagnostics.CodeAnalysis; using StardewModdingAPI.Events; +using StardewModdingAPI.Framework.PerformanceCounter; namespace StardewModdingAPI.Framework.Events { @@ -173,10 +174,10 @@ namespace StardewModdingAPI.Framework.Events /// Construct an instance. /// Writes messages to the log. /// The mod registry with which to identify mods. - public EventManager(IMonitor monitor, ModRegistry modRegistry) + public EventManager(IMonitor monitor, ModRegistry modRegistry, PerformanceCounterManager performanceCounterManager) { // create shortcut initializers - ManagedEvent ManageEventOf(string typeName, string eventName) => new ManagedEvent($"{typeName}.{eventName}", monitor, modRegistry); + ManagedEvent ManageEventOf(string typeName, string eventName) => new ManagedEvent($"{typeName}.{eventName}", monitor, modRegistry, performanceCounterManager); // init events (new) this.MenuChanged = ManageEventOf(nameof(IModEvents.Display), nameof(IDisplayEvents.MenuChanged)); diff --git a/src/SMAPI/Framework/Events/IManagedEvent.cs b/src/SMAPI/Framework/Events/IManagedEvent.cs new file mode 100644 index 00000000..04476866 --- /dev/null +++ b/src/SMAPI/Framework/Events/IManagedEvent.cs @@ -0,0 +1,7 @@ +namespace StardewModdingAPI.Framework.Events +{ + internal interface IManagedEvent + { + string GetName(); + } +} diff --git a/src/SMAPI/Framework/Events/ManagedEvent.cs b/src/SMAPI/Framework/Events/ManagedEvent.cs index bb915738..bba94c35 100644 --- a/src/SMAPI/Framework/Events/ManagedEvent.cs +++ b/src/SMAPI/Framework/Events/ManagedEvent.cs @@ -1,15 +1,13 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Linq; -using StardewModdingAPI.Framework.Utilities; -using PerformanceCounter = StardewModdingAPI.Framework.PerformanceCounter.PerformanceCounter; +using PerformanceCounterManager = StardewModdingAPI.Framework.PerformanceCounter.PerformanceCounterManager; namespace StardewModdingAPI.Framework.Events { /// An event wrapper which intercepts and logs errors in handler code. /// The event arguments type. - internal class ManagedEvent: IPerformanceCounterEvent + internal class ManagedEvent: IManagedEvent { /********* ** Fields @@ -32,38 +30,8 @@ namespace StardewModdingAPI.Framework.Events /// The cached invocation list. private EventHandler[] CachedInvocationList; - public IDictionary PerformanceCounters { get; } = new Dictionary(); - - private readonly Stopwatch Stopwatch = new Stopwatch(); - - private long EventCallCount = 0; - - private readonly DateTime StartDateTime = DateTime.Now; - - public string GetEventName() - { - return this.EventName; - } - - public double GetGameAverageExecutionTime() - { - if (this.PerformanceCounters.TryGetValue(Constants.GamePerformanceCounterName, out PerformanceCounter.PerformanceCounter gameExecTime)) - { - return gameExecTime.GetAverage(); - } - - return 0; - } - - public double GetModsAverageExecutionTime() - { - return this.PerformanceCounters.Where(p => p.Key != Constants.GamePerformanceCounterName).Sum(p => p.Value.GetAverage()); - } - - public double GetAverageExecutionTime() - { - return this.PerformanceCounters.Sum(p => p.Value.GetAverage()); - } + /// The performance counter manager. + private readonly PerformanceCounterManager PerformanceCounterManager; /********* ** Public methods @@ -72,11 +40,18 @@ namespace StardewModdingAPI.Framework.Events /// A human-readable name for the event. /// Writes messages to the log. /// The mod registry with which to identify mods. - public ManagedEvent(string eventName, IMonitor monitor, ModRegistry modRegistry) + public ManagedEvent(string eventName, IMonitor monitor, ModRegistry modRegistry, PerformanceCounterManager performanceCounterManager) { this.EventName = eventName; this.Monitor = monitor; this.ModRegistry = modRegistry; + this.PerformanceCounterManager = performanceCounterManager; + } + + /// Gets the event name. + public string GetName() + { + return this.EventName; } /// Get whether anything is listening to the event. @@ -99,8 +74,6 @@ namespace StardewModdingAPI.Framework.Events { this.Event += handler; this.AddTracking(mod, handler, this.Event?.GetInvocationList().Cast>()); - - } /// Remove an event handler. @@ -111,18 +84,6 @@ namespace StardewModdingAPI.Framework.Events this.RemoveTracking(handler, this.Event?.GetInvocationList().Cast>()); } - public long GetAverageCallsPerSecond() - { - long runtimeInSeconds = (long)DateTime.Now.Subtract(this.StartDateTime).TotalSeconds; - - if (runtimeInSeconds == 0) - { - return 0; - } - - return this.EventCallCount / runtimeInSeconds; - } - /// Raise the event and notify all handlers. /// The event arguments to pass. public void Raise(TEventArgs args) @@ -130,37 +91,23 @@ namespace StardewModdingAPI.Framework.Events if (this.Event == null) return; - this.EventCallCount++; + + this.PerformanceCounterManager.BeginTrackInvocation(this.EventName); foreach (EventHandler handler in this.CachedInvocationList) { try { - var performanceCounterEntry = new PerformanceCounterEntry() - { - EventTime = DateTime.Now - }; - - this.Stopwatch.Reset(); - this.Stopwatch.Start(); - handler.Invoke(null, args); - this.Stopwatch.Stop(); - performanceCounterEntry.Elapsed = this.Stopwatch.Elapsed; - - string modName = this.GetSourceMod(handler)?.DisplayName ?? Constants.GamePerformanceCounterName; - - if (!this.PerformanceCounters.ContainsKey(modName)) - { - this.PerformanceCounters.Add(modName, new PerformanceCounter.PerformanceCounter($"{modName}.{this.EventName}")); - } - this.PerformanceCounters[modName].Add(performanceCounterEntry); - + this.PerformanceCounterManager.Track(this.EventName, this.GetModNameForPerformanceCounters(handler), + () => handler.Invoke(null, args)); } catch (Exception ex) { this.LogError(handler, ex); } } + + this.PerformanceCounterManager.EndTrackInvocation(this.EventName); } /// Raise the event and notify all handlers. @@ -191,6 +138,20 @@ namespace StardewModdingAPI.Framework.Events /********* ** Private methods *********/ + + private string GetModNameForPerformanceCounters(EventHandler handler) + { + IModMetadata mod = this.GetSourceMod(handler); + + if (mod == null) + { + return Constants.GamePerformanceCounterName; + } + + return mod.HasManifest() ? mod.Manifest.UniqueID : mod.DisplayName; + + } + /// Track an event handler. /// The mod which added the handler. /// The event handler. diff --git a/src/SMAPI/Framework/PerformanceCounter/AlertContext.cs b/src/SMAPI/Framework/PerformanceCounter/AlertContext.cs new file mode 100644 index 00000000..c4a57a49 --- /dev/null +++ b/src/SMAPI/Framework/PerformanceCounter/AlertContext.cs @@ -0,0 +1,14 @@ +namespace StardewModdingAPI.Framework.PerformanceCounter +{ + public struct AlertContext + { + public string Source; + public double Elapsed; + + public AlertContext(string source, double elapsed) + { + this.Source = source; + this.Elapsed = elapsed; + } + } +} diff --git a/src/SMAPI/Framework/PerformanceCounter/AlertEntry.cs b/src/SMAPI/Framework/PerformanceCounter/AlertEntry.cs new file mode 100644 index 00000000..284af1ce --- /dev/null +++ b/src/SMAPI/Framework/PerformanceCounter/AlertEntry.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; + +namespace StardewModdingAPI.Framework.PerformanceCounter +{ + internal struct AlertEntry + { + public PerformanceCounterCollection Collection; + public double ExecutionTimeMilliseconds; + public double Threshold; + public List Context; + + public AlertEntry(PerformanceCounterCollection collection, double executionTimeMilliseconds, double threshold, List context) + { + this.Collection = collection; + this.ExecutionTimeMilliseconds = executionTimeMilliseconds; + this.Threshold = threshold; + this.Context = context; + } + } +} diff --git a/src/SMAPI/Framework/PerformanceCounter/EventPerformanceCounterCategory.cs b/src/SMAPI/Framework/PerformanceCounter/EventPerformanceCounterCategory.cs deleted file mode 100644 index 14f74317..00000000 --- a/src/SMAPI/Framework/PerformanceCounter/EventPerformanceCounterCategory.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace StardewModdingAPI.Framework.Utilities -{ - public class EventPerformanceCounterCategory - { - public IPerformanceCounterEvent Event { get; } - public double MonitorThreshold { get; } - public bool IsImportant { get; } - public bool Monitor { get; } - - public EventPerformanceCounterCategory(IPerformanceCounterEvent @event, bool isImportant) - { - this.Event = @event; - this.IsImportant = isImportant; - } - } -} diff --git a/src/SMAPI/Framework/PerformanceCounter/EventPerformanceCounterCollection.cs b/src/SMAPI/Framework/PerformanceCounter/EventPerformanceCounterCollection.cs new file mode 100644 index 00000000..1aec28f3 --- /dev/null +++ b/src/SMAPI/Framework/PerformanceCounter/EventPerformanceCounterCollection.cs @@ -0,0 +1,11 @@ +using StardewModdingAPI.Framework.Events; + +namespace StardewModdingAPI.Framework.PerformanceCounter +{ + internal class EventPerformanceCounterCollection: PerformanceCounterCollection + { + public EventPerformanceCounterCollection(PerformanceCounterManager manager, IManagedEvent @event, bool isImportant) : base(manager, @event.GetName(), isImportant) + { + } + } +} diff --git a/src/SMAPI/Framework/PerformanceCounter/IPerformanceCounterEvent.cs b/src/SMAPI/Framework/PerformanceCounter/IPerformanceCounterEvent.cs index 6b83586d..1bcf4fa0 100644 --- a/src/SMAPI/Framework/PerformanceCounter/IPerformanceCounterEvent.cs +++ b/src/SMAPI/Framework/PerformanceCounter/IPerformanceCounterEvent.cs @@ -7,7 +7,6 @@ namespace StardewModdingAPI.Framework.Utilities { string GetEventName(); long GetAverageCallsPerSecond(); - IDictionary PerformanceCounters { get; } double GetGameAverageExecutionTime(); double GetModsAverageExecutionTime(); diff --git a/src/SMAPI/Framework/PerformanceCounter/PerformanceCounter.cs b/src/SMAPI/Framework/PerformanceCounter/PerformanceCounter.cs index 0b0275b7..3dbc693a 100644 --- a/src/SMAPI/Framework/PerformanceCounter/PerformanceCounter.cs +++ b/src/SMAPI/Framework/PerformanceCounter/PerformanceCounter.cs @@ -6,22 +6,25 @@ using StardewModdingAPI.Framework.Utilities; namespace StardewModdingAPI.Framework.PerformanceCounter { - public class PerformanceCounter + internal class PerformanceCounter { private const int MAX_ENTRIES = 16384; - public string Name { get; } + public string Source { get; } public static Stopwatch Stopwatch = new Stopwatch(); public static long TotalNumEventsLogged; - + public double MonitorThresholdMilliseconds { get; set; } + public bool Monitor { get; set; } + private readonly PerformanceCounterCollection ParentCollection; private readonly CircularBuffer _counter; private PerformanceCounterEntry? PeakPerformanceCounterEntry; - public PerformanceCounter(string name) + public PerformanceCounter(PerformanceCounterCollection parentCollection, string source) { - this.Name = name; + this.ParentCollection = parentCollection; + this.Source = source; this._counter = new CircularBuffer(PerformanceCounter.MAX_ENTRIES); } @@ -47,6 +50,11 @@ namespace StardewModdingAPI.Framework.PerformanceCounter PerformanceCounter.Stopwatch.Start(); this._counter.Put(entry); + if (this.Monitor && entry.Elapsed.TotalMilliseconds > this.MonitorThresholdMilliseconds) + { + this.ParentCollection.AddAlert(entry.Elapsed.TotalMilliseconds, this.MonitorThresholdMilliseconds, new AlertContext(this.Source, entry.Elapsed.TotalMilliseconds)); + } + if (this.PeakPerformanceCounterEntry == null) { this.PeakPerformanceCounterEntry = entry; diff --git a/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterCollection.cs b/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterCollection.cs new file mode 100644 index 00000000..343fddf6 --- /dev/null +++ b/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterCollection.cs @@ -0,0 +1,144 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using StardewModdingAPI.Framework.Utilities; + +namespace StardewModdingAPI.Framework.PerformanceCounter +{ + internal class PerformanceCounterCollection + { + public IDictionary PerformanceCounters { get; } = new Dictionary(); + private DateTime StartDateTime = DateTime.Now; + private long CallCount; + public string Name { get; private set; } + public bool IsImportant { get; set; } + private readonly Stopwatch Stopwatch = new Stopwatch(); + private readonly PerformanceCounterManager PerformanceCounterManager; + public double MonitorThresholdMilliseconds { get; set; } + public bool Monitor { get; set; } + private readonly List TriggeredPerformanceCounters = new List(); + + public PerformanceCounterCollection(PerformanceCounterManager performanceCounterManager, string name, bool isImportant) + { + this.Name = name; + this.PerformanceCounterManager = performanceCounterManager; + this.IsImportant = isImportant; + } + + public PerformanceCounterCollection(PerformanceCounterManager performanceCounterManager, string name) + { + this.PerformanceCounterManager = performanceCounterManager; + this.Name = name; + } + + public void Track(string source, PerformanceCounterEntry entry) + { + if (!this.PerformanceCounters.ContainsKey(source)) + { + this.PerformanceCounters.Add(source, new PerformanceCounter(this, source)); + } + this.PerformanceCounters[source].Add(entry); + + if (this.Monitor) + { + this.TriggeredPerformanceCounters.Add(new AlertContext(source, entry.Elapsed.TotalMilliseconds)); + } + } + + public double GetModsAverageExecutionTime() + { + return this.PerformanceCounters.Where(p => p.Key != Constants.GamePerformanceCounterName).Sum(p => p.Value.GetAverage()); + } + + public double GetAverageExecutionTime() + { + return this.PerformanceCounters.Sum(p => p.Value.GetAverage()); + } + + public double GetGameAverageExecutionTime() + { + if (this.PerformanceCounters.TryGetValue(Constants.GamePerformanceCounterName, out PerformanceCounter gameExecTime)) + { + return gameExecTime.GetAverage(); + } + + return 0; + } + + public void BeginTrackInvocation() + { + if (this.Monitor) + { + this.TriggeredPerformanceCounters.Clear(); + this.Stopwatch.Reset(); + this.Stopwatch.Start(); + } + + this.CallCount++; + + } + + public void EndTrackInvocation() + { + if (!this.Monitor) return; + + this.Stopwatch.Stop(); + if (this.Stopwatch.Elapsed.TotalMilliseconds >= this.MonitorThresholdMilliseconds) + { + this.AddAlert(this.Stopwatch.Elapsed.TotalMilliseconds, + this.MonitorThresholdMilliseconds, this.TriggeredPerformanceCounters); + } + } + + public void AddAlert(double executionTimeMilliseconds, double threshold, List alerts) + { + this.PerformanceCounterManager.AddAlert(new AlertEntry(this, executionTimeMilliseconds, + threshold, alerts)); + } + + public void AddAlert(double executionTimeMilliseconds, double threshold, AlertContext alert) + { + this.AddAlert(executionTimeMilliseconds, threshold, new List() {alert}); + } + + public void ResetCallsPerSecond() + { + this.CallCount = 0; + this.StartDateTime = DateTime.Now; + } + + public void Reset() + { + foreach (var i in this.PerformanceCounters) + { + i.Value.Reset(); + i.Value.ResetPeak(); + } + } + + public void ResetSource(string source) + { + foreach (var i in this.PerformanceCounters) + { + if (i.Value.Source.Equals(source, StringComparison.InvariantCultureIgnoreCase)) + { + i.Value.Reset(); + i.Value.ResetPeak(); + } + } + } + + public long GetAverageCallsPerSecond() + { + long runtimeInSeconds = (long) DateTime.Now.Subtract(this.StartDateTime).TotalSeconds; + + if (runtimeInSeconds == 0) + { + return 0; + } + + return this.CallCount / runtimeInSeconds; + } + } +} diff --git a/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterManager.cs b/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterManager.cs index ae7258e2..9e77e2fa 100644 --- a/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterManager.cs +++ b/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterManager.cs @@ -1,4 +1,8 @@ +using System; using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; using StardewModdingAPI.Framework.Events; using StardewModdingAPI.Framework.Utilities; @@ -6,92 +10,187 @@ namespace StardewModdingAPI.Framework.PerformanceCounter { internal class PerformanceCounterManager { - public HashSet PerformanceCounterEvents = new HashSet(); + public HashSet PerformanceCounterCollections = new HashSet(); + public List Alerts = new List(); + private readonly IMonitor Monitor; + private readonly Stopwatch Stopwatch = new Stopwatch(); - private readonly EventManager EventManager; - - public PerformanceCounterManager(EventManager eventManager) + public PerformanceCounterManager(IMonitor monitor) { - this.EventManager = eventManager; - this.InitializePerformanceCounterEvents(); + this.Monitor = monitor; } public void Reset() { - foreach (var performanceCounter in this.PerformanceCounterEvents) + foreach (var performanceCounter in this.PerformanceCounterCollections) { - this.ResetCategory(performanceCounter); + foreach (var eventPerformanceCounter in performanceCounter.PerformanceCounters) + { + eventPerformanceCounter.Value.Reset(); + } } } - public void ResetCategory(EventPerformanceCounterCategory category) + /// Print any queued messages. + public void PrintQueued() { - foreach (var eventPerformanceCounter in category.Event.PerformanceCounters) + if (this.Alerts.Count == 0) + { + return; + } + StringBuilder sb = new StringBuilder(); + + foreach (var alert in this.Alerts) { - eventPerformanceCounter.Value.Reset(); + sb.AppendLine($"{alert.Collection.Name} took {alert.ExecutionTimeMilliseconds:F2}ms (exceeded threshold of {alert.Threshold:F2}ms)"); + + foreach (var context in alert.Context) + { + sb.AppendLine($"{context.Source}: {context.Elapsed:F2}ms"); + } } + + this.Alerts.Clear(); + + this.Monitor.Log(sb.ToString(), LogLevel.Error); + } + + public void BeginTrackInvocation(string collectionName) + { + this.GetOrCreateCollectionByName(collectionName).BeginTrackInvocation(); + } + + public void EndTrackInvocation(string collectionName) + { + this.GetOrCreateCollectionByName(collectionName).EndTrackInvocation(); } - private void InitializePerformanceCounterEvents() + public void Track(string collectionName, string modName, Action action) { - this.PerformanceCounterEvents = new HashSet() + DateTime eventTime = DateTime.UtcNow; + this.Stopwatch.Reset(); + this.Stopwatch.Start(); + + try + { + action(); + } + finally { - new EventPerformanceCounterCategory(this.EventManager.MenuChanged, false), + this.Stopwatch.Stop(); - // Rendering Events - new EventPerformanceCounterCategory(this.EventManager.Rendering, true), - new EventPerformanceCounterCategory(this.EventManager.Rendered, true), - new EventPerformanceCounterCategory(this.EventManager.RenderingWorld, true), - new EventPerformanceCounterCategory(this.EventManager.RenderedWorld, true), - new EventPerformanceCounterCategory(this.EventManager.RenderingActiveMenu, true), - new EventPerformanceCounterCategory(this.EventManager.RenderedActiveMenu, true), - new EventPerformanceCounterCategory(this.EventManager.RenderingHud, true), - new EventPerformanceCounterCategory(this.EventManager.RenderedHud, true), - - new EventPerformanceCounterCategory(this.EventManager.WindowResized, false), - new EventPerformanceCounterCategory(this.EventManager.GameLaunched, false), - new EventPerformanceCounterCategory(this.EventManager.UpdateTicking, true), - new EventPerformanceCounterCategory(this.EventManager.UpdateTicked, true), - new EventPerformanceCounterCategory(this.EventManager.OneSecondUpdateTicking, true), - new EventPerformanceCounterCategory(this.EventManager.OneSecondUpdateTicked, true), - - new EventPerformanceCounterCategory(this.EventManager.SaveCreating, false), - new EventPerformanceCounterCategory(this.EventManager.SaveCreated, false), - new EventPerformanceCounterCategory(this.EventManager.Saving, false), - new EventPerformanceCounterCategory(this.EventManager.Saved, false), - - new EventPerformanceCounterCategory(this.EventManager.DayStarted, false), - new EventPerformanceCounterCategory(this.EventManager.DayEnding, false), - - new EventPerformanceCounterCategory(this.EventManager.TimeChanged, true), - - new EventPerformanceCounterCategory(this.EventManager.ReturnedToTitle, false), - - new EventPerformanceCounterCategory(this.EventManager.ButtonPressed, true), - new EventPerformanceCounterCategory(this.EventManager.ButtonReleased, true), - new EventPerformanceCounterCategory(this.EventManager.CursorMoved, true), - new EventPerformanceCounterCategory(this.EventManager.MouseWheelScrolled, true), - - new EventPerformanceCounterCategory(this.EventManager.PeerContextReceived, true), - new EventPerformanceCounterCategory(this.EventManager.ModMessageReceived, true), - new EventPerformanceCounterCategory(this.EventManager.PeerDisconnected, true), - new EventPerformanceCounterCategory(this.EventManager.InventoryChanged, true), - new EventPerformanceCounterCategory(this.EventManager.LevelChanged, true), - new EventPerformanceCounterCategory(this.EventManager.Warped, true), - - new EventPerformanceCounterCategory(this.EventManager.LocationListChanged, true), - new EventPerformanceCounterCategory(this.EventManager.BuildingListChanged, true), - new EventPerformanceCounterCategory(this.EventManager.LocationListChanged, true), - new EventPerformanceCounterCategory(this.EventManager.DebrisListChanged, true), - new EventPerformanceCounterCategory(this.EventManager.LargeTerrainFeatureListChanged, true), - new EventPerformanceCounterCategory(this.EventManager.NpcListChanged, true), - new EventPerformanceCounterCategory(this.EventManager.ObjectListChanged, true), - new EventPerformanceCounterCategory(this.EventManager.ChestInventoryChanged, true), - new EventPerformanceCounterCategory(this.EventManager.TerrainFeatureListChanged, true), - new EventPerformanceCounterCategory(this.EventManager.LoadStageChanged, false), - new EventPerformanceCounterCategory(this.EventManager.UnvalidatedUpdateTicking, true), - new EventPerformanceCounterCategory(this.EventManager.UnvalidatedUpdateTicked, true), + this.GetOrCreateCollectionByName(collectionName).Track(modName, new PerformanceCounterEntry + { + EventTime = eventTime, + Elapsed = this.Stopwatch.Elapsed + }); + } + } + + public PerformanceCounterCollection GetCollectionByName(string name) + { + return this.PerformanceCounterCollections.FirstOrDefault(collection => collection.Name == name); + } + + public PerformanceCounterCollection GetOrCreateCollectionByName(string name) + { + PerformanceCounterCollection collection = this.GetCollectionByName(name); + if (collection == null) + { + collection = new PerformanceCounterCollection(this, name); + this.PerformanceCounterCollections.Add(collection); + } + + return collection; + } + + public void ResetCategory(string name) + { + foreach (var performanceCounterCollection in this.PerformanceCounterCollections) + { + if (performanceCounterCollection.Name.Equals(name, StringComparison.InvariantCultureIgnoreCase)) + { + performanceCounterCollection.ResetCallsPerSecond(); + performanceCounterCollection.Reset(); + } + } + } + + public void ResetSource(string name) + { + foreach (var performanceCounterCollection in this.PerformanceCounterCollections) + { + performanceCounterCollection.ResetSource(name); + } + } + + + public void AddAlert(AlertEntry entry) + { + this.Alerts.Add(entry); + } + + public void InitializePerformanceCounterEvents(EventManager eventManager) + { + this.PerformanceCounterCollections = new HashSet() + { + new EventPerformanceCounterCollection(this, eventManager.MenuChanged, false), + + + // Rendering Events + new EventPerformanceCounterCollection(this, eventManager.Rendering, true), + new EventPerformanceCounterCollection(this, eventManager.Rendered, true), + new EventPerformanceCounterCollection(this, eventManager.RenderingWorld, true), + new EventPerformanceCounterCollection(this, eventManager.RenderedWorld, true), + new EventPerformanceCounterCollection(this, eventManager.RenderingActiveMenu, true), + new EventPerformanceCounterCollection(this, eventManager.RenderedActiveMenu, true), + new EventPerformanceCounterCollection(this, eventManager.RenderingHud, true), + new EventPerformanceCounterCollection(this, eventManager.RenderedHud, true), + + new EventPerformanceCounterCollection(this, eventManager.WindowResized, false), + new EventPerformanceCounterCollection(this, eventManager.GameLaunched, false), + new EventPerformanceCounterCollection(this, eventManager.UpdateTicking, true), + new EventPerformanceCounterCollection(this, eventManager.UpdateTicked, true), + new EventPerformanceCounterCollection(this, eventManager.OneSecondUpdateTicking, true), + new EventPerformanceCounterCollection(this, eventManager.OneSecondUpdateTicked, true), + + new EventPerformanceCounterCollection(this, eventManager.SaveCreating, false), + new EventPerformanceCounterCollection(this, eventManager.SaveCreated, false), + new EventPerformanceCounterCollection(this, eventManager.Saving, false), + new EventPerformanceCounterCollection(this, eventManager.Saved, false), + + new EventPerformanceCounterCollection(this, eventManager.DayStarted, false), + new EventPerformanceCounterCollection(this, eventManager.DayEnding, false), + + new EventPerformanceCounterCollection(this, eventManager.TimeChanged, true), + + new EventPerformanceCounterCollection(this, eventManager.ReturnedToTitle, false), + + new EventPerformanceCounterCollection(this, eventManager.ButtonPressed, true), + new EventPerformanceCounterCollection(this, eventManager.ButtonReleased, true), + new EventPerformanceCounterCollection(this, eventManager.CursorMoved, true), + new EventPerformanceCounterCollection(this, eventManager.MouseWheelScrolled, true), + + new EventPerformanceCounterCollection(this, eventManager.PeerContextReceived, true), + new EventPerformanceCounterCollection(this, eventManager.ModMessageReceived, true), + new EventPerformanceCounterCollection(this, eventManager.PeerDisconnected, true), + new EventPerformanceCounterCollection(this, eventManager.InventoryChanged, true), + new EventPerformanceCounterCollection(this, eventManager.LevelChanged, true), + new EventPerformanceCounterCollection(this, eventManager.Warped, true), + + new EventPerformanceCounterCollection(this, eventManager.LocationListChanged, true), + new EventPerformanceCounterCollection(this, eventManager.BuildingListChanged, true), + new EventPerformanceCounterCollection(this, eventManager.LocationListChanged, true), + new EventPerformanceCounterCollection(this, eventManager.DebrisListChanged, true), + new EventPerformanceCounterCollection(this, eventManager.LargeTerrainFeatureListChanged, true), + new EventPerformanceCounterCollection(this, eventManager.NpcListChanged, true), + new EventPerformanceCounterCollection(this, eventManager.ObjectListChanged, true), + new EventPerformanceCounterCollection(this, eventManager.ChestInventoryChanged, true), + new EventPerformanceCounterCollection(this, eventManager.TerrainFeatureListChanged, true), + new EventPerformanceCounterCollection(this, eventManager.LoadStageChanged, false), + new EventPerformanceCounterCollection(this, eventManager.UnvalidatedUpdateTicking, false), + new EventPerformanceCounterCollection(this, eventManager.UnvalidatedUpdateTicked, false), }; } } diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index d1dba9ea..5b0c6691 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -25,7 +25,6 @@ using StardewModdingAPI.Framework.ModLoading; using StardewModdingAPI.Framework.Patching; using StardewModdingAPI.Framework.Reflection; using StardewModdingAPI.Framework.Serialization; -using StardewModdingAPI.Framework.Utilities; using StardewModdingAPI.Patches; using StardewModdingAPI.Toolkit; using StardewModdingAPI.Toolkit.Framework.Clients.WebApi; @@ -82,8 +81,6 @@ namespace StardewModdingAPI.Framework /// Manages SMAPI events for mods. private readonly EventManager EventManager; - private readonly PerformanceCounterManager PerformanceCounterManager; - /// Whether the game is currently running. private bool IsGameRunning; @@ -137,6 +134,10 @@ namespace StardewModdingAPI.Framework /// This is initialized after the game starts. This is accessed directly because it's not part of the normal class model. internal static DeprecationManager DeprecationManager { get; private set; } + /// Manages performance counters. + /// This is initialized after the game starts. This is accessed directly because it's not part of the normal class model. + internal static PerformanceCounterManager PerformanceCounterManager { get; private set; } + /********* ** Public methods @@ -165,8 +166,10 @@ namespace StardewModdingAPI.Framework ShowFullStampInConsole = this.Settings.DeveloperMode }; this.MonitorForGame = this.GetSecondaryMonitor("game"); - this.EventManager = new EventManager(this.Monitor, this.ModRegistry); - this.PerformanceCounterManager = new PerformanceCounterManager(this.EventManager); + + SCore.PerformanceCounterManager = new PerformanceCounterManager(this.Monitor); + this.EventManager = new EventManager(this.Monitor, this.ModRegistry, SCore.PerformanceCounterManager); + SCore.PerformanceCounterManager.InitializePerformanceCounterEvents(this.EventManager); SCore.DeprecationManager = new DeprecationManager(this.Monitor, this.ModRegistry); @@ -245,6 +248,7 @@ namespace StardewModdingAPI.Framework jsonHelper: this.Toolkit.JsonHelper, modRegistry: this.ModRegistry, deprecationManager: SCore.DeprecationManager, + performanceCounterManager: SCore.PerformanceCounterManager, onGameInitialized: this.InitializeAfterGameStart, onGameExiting: this.Dispose, cancellationToken: this.CancellationToken, @@ -488,20 +492,6 @@ namespace StardewModdingAPI.Framework this.Monitor.Log("Type 'help' for help, or 'help ' for a command's usage", LogLevel.Info); this.GameInstance.CommandManager.Add(null, "help", "Lists command documentation.\n\nUsage: help\nLists all available commands.\n\nUsage: help \n- cmd: The name of a command whose documentation to display.", this.HandleCommand); this.GameInstance.CommandManager.Add(null, "reload_i18n", "Reloads translation files for all mods.\n\nUsage: reload_i18n", this.HandleCommand); - this.GameInstance.CommandManager.Add(null, "performance_counters", - "Displays performance counters.\n\n"+ - "Usage: performance_counters\n" + - "Shows the most important event invocation times\n\n"+ - "Usage: performance_counters summary|sum [all|important|name]\n"+ - "- summary or sum: Forces summary mode\n"+ - "- all, important or name: Displays all event performance counters, only important ones, or a specific event by name (defaults to important)\n\n"+ - "Usage: performance_counters [name] [threshold]\n"+ - "Shows detailed performance counters for a specific event\n"+ - "- name: The (partial) name of the event\n"+ - "- threshold: The minimum avg execution time (ms) of the event\n\n"+ - "Usage: performance_counters reset\n"+ - "Resets all performance counters\n", this.HandleCommand); - this.GameInstance.CommandManager.Add(null, "pc", "Alias for performance_counters", this.HandleCommand); // start handling command line input Thread inputThread = new Thread(() => @@ -1317,176 +1307,11 @@ namespace StardewModdingAPI.Framework this.ReloadTranslations(this.ModRegistry.GetAll(contentPacks: false)); this.Monitor.Log("Reloaded translation files for all mods. This only affects new translations the mods fetch; if they cached some text, it may not be updated.", LogLevel.Info); break; - case "performance_counters": - case "pc": - this.DisplayPerformanceCounters(arguments.ToList()); - break; default: throw new NotSupportedException($"Unrecognized core SMAPI command '{name}'."); } } - /// Get an ASCII table to show tabular data in the console. - /// The data type. - /// The data to display. - /// The table header. - /// Returns a set of fields for a data value. - protected string GetTableString(IEnumerable data, string[] header, Func getRow) - { - // get table data - int[] widths = header.Select(p => p.Length).ToArray(); - string[][] rows = data - .Select(item => - { - string[] fields = getRow(item); - if (fields.Length != widths.Length) - throw new InvalidOperationException($"Expected {widths.Length} columns, but found {fields.Length}: {string.Join(", ", fields)}"); - - for (int i = 0; i < fields.Length; i++) - widths[i] = Math.Max(widths[i], fields[i].Length); - - return fields; - }) - .ToArray(); - - // render fields - List lines = new List(rows.Length + 2) - { - header, - header.Select((value, i) => "".PadRight(widths[i], '-')).ToArray() - }; - lines.AddRange(rows); - - return string.Join( - Environment.NewLine, - lines.Select(line => string.Join(" | ", line.Select((field, i) => field.PadLeft(widths[i], ' ')).ToArray()) - ) - ); - } - - private void DisplayPerformanceCounters(IList arguments) - { - bool showSummary = true; - bool showSummaryOnlyImportant = true; - string filterByName = null; - - if (arguments.Any()) - { - switch (arguments[0]) - { - case "summary": - case "sum": - showSummary = true; - - if (arguments.Count > 1) - { - switch (arguments[1].ToLower()) - { - case "all": - showSummaryOnlyImportant = false; - break; - case "important": - showSummaryOnlyImportant = true; - break; - default: - filterByName = arguments[1]; - break; - } - } - break; - case "reset": - this.PerformanceCounterManager.Reset(); - return; - default: - showSummary = false; - filterByName = arguments[0]; - break; - - } - } - var lastMinute = TimeSpan.FromSeconds(60); - - if (showSummary) - { - this.DisplayPerformanceCounterSummary(showSummaryOnlyImportant, filterByName); - } - else - { - var data = this.PerformanceCounterManager.PerformanceCounterEvents.Where(p => p.Event.GetEventName().ToLowerInvariant().Contains(filterByName.ToLowerInvariant())); - - foreach (var i in data) - { - this.DisplayPerformanceCounter(i, lastMinute); - } - } - - double avgTime = PerformanceCounter.PerformanceCounter.Stopwatch.ElapsedMilliseconds / (double)PerformanceCounter.PerformanceCounter.TotalNumEventsLogged; - this.Monitor.Log($"Logged {PerformanceCounter.PerformanceCounter.TotalNumEventsLogged} events in {PerformanceCounter.PerformanceCounter.Stopwatch.ElapsedMilliseconds}ms (avg {avgTime:F4}ms / event)"); - - } - - private void DisplayPerformanceCounterSummary(bool showOnlyImportant, string eventNameFilter = null) - { - StringBuilder sb = new StringBuilder($"Performance Counter Summary:\n\n"); - - IEnumerable data; - - if (eventNameFilter != null) - { - data = this.PerformanceCounterManager.PerformanceCounterEvents.Where(p => p.Event.GetEventName().ToLowerInvariant().Contains(eventNameFilter.ToLowerInvariant())); - } - else - { - if (showOnlyImportant) - { - data = this.PerformanceCounterManager.PerformanceCounterEvents.Where(p => p.IsImportant); - } - else - { - data = this.PerformanceCounterManager.PerformanceCounterEvents; - } - } - - - sb.AppendLine(this.GetTableString( - data: data, - header: new[] {"Event", "Avg Calls/s", "Avg Execution Time (Game)", "Avg Execution Time (Mods)", "Avg Execution Time (Game+Mods)"}, - getRow: item => new[] - { - item.Event.GetEventName(), - item.Event.GetAverageCallsPerSecond().ToString(), - item.Event.GetGameAverageExecutionTime().ToString("F2") + " ms", - item.Event.GetModsAverageExecutionTime().ToString("F2") + " ms", - item.Event.GetAverageExecutionTime().ToString("F2") + " ms" - } - )); - - this.Monitor.Log(sb.ToString(), LogLevel.Info); - } - - private void DisplayPerformanceCounter (EventPerformanceCounterCategory obj, TimeSpan averageInterval) - { - StringBuilder sb = new StringBuilder($"Performance Counter for {obj.Event.GetEventName()}:\n\n"); - - sb.AppendLine(this.GetTableString( - data: obj.Event.PerformanceCounters, - header: new[] {"Mod", $"Avg Execution Time over {(int)averageInterval.TotalSeconds}s", "Last Execution Time", "Peak Execution Time"}, - getRow: item => new[] - { - item.Key, - item.Value.GetAverage(averageInterval).ToString("F2") + " ms" ?? "-", - item.Value.GetLastEntry()?.Elapsed.TotalMilliseconds.ToString("F2") + " ms" ?? "-", - item.Value.GetPeak()?.Elapsed.TotalMilliseconds.ToString("F2") + " ms" ?? "-", - } - )); - - sb.AppendLine($"Average execution time (Game+Mods): {obj.Event.GetAverageExecutionTime():F2} ms"); - sb.AppendLine($"Average execution time (Game only) : {obj.Event.GetGameAverageExecutionTime():F2} ms"); - sb.AppendLine($"Average execution time (Mods only) : {obj.Event.GetModsAverageExecutionTime():F2} ms"); - - this.Monitor.Log(sb.ToString(), LogLevel.Info); - } - /// Redirect messages logged directly to the console to the given monitor. /// The monitor with which to log messages as the game. /// The message to log. diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index d6c3b836..8aba9b57 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -17,6 +17,7 @@ using StardewModdingAPI.Framework.Content; using StardewModdingAPI.Framework.Events; using StardewModdingAPI.Framework.Input; using StardewModdingAPI.Framework.Networking; +using StardewModdingAPI.Framework.PerformanceCounter; using StardewModdingAPI.Framework.Reflection; using StardewModdingAPI.Framework.StateTracking.Comparers; using StardewModdingAPI.Framework.StateTracking.Snapshots; @@ -58,6 +59,8 @@ namespace StardewModdingAPI.Framework /// Manages deprecation warnings. private readonly DeprecationManager DeprecationManager; + private readonly PerformanceCounterManager PerformanceCounterManager; + /// The maximum number of consecutive attempts SMAPI should make to recover from a draw error. private readonly Countdown DrawCrashTimer = new Countdown(60); // 60 ticks = roughly one second @@ -152,11 +155,12 @@ namespace StardewModdingAPI.Framework /// Encapsulates SMAPI's JSON file parsing. /// Tracks the installed mods. /// Manages deprecation warnings. + /// Manages performance monitoring. /// A callback to invoke after the game finishes initializing. /// A callback to invoke when the game exits. /// Propagates notification that SMAPI should exit. /// Whether to log network traffic. - internal SGame(Monitor monitor, IMonitor monitorForGame, Reflector reflection, Translator translator, EventManager eventManager, JsonHelper jsonHelper, ModRegistry modRegistry, DeprecationManager deprecationManager, Action onGameInitialized, Action onGameExiting, CancellationTokenSource cancellationToken, bool logNetworkTraffic) + internal SGame(Monitor monitor, IMonitor monitorForGame, Reflector reflection, Translator translator, EventManager eventManager, JsonHelper jsonHelper, ModRegistry modRegistry, DeprecationManager deprecationManager, PerformanceCounterManager performanceCounterManager, Action onGameInitialized, Action onGameExiting, CancellationTokenSource cancellationToken, bool logNetworkTraffic) { this.OnLoadingFirstAsset = SGame.ConstructorHack.OnLoadingFirstAsset; SGame.ConstructorHack = null; @@ -176,6 +180,7 @@ namespace StardewModdingAPI.Framework this.Reflection = reflection; this.Translator = translator; this.DeprecationManager = deprecationManager; + this.PerformanceCounterManager = performanceCounterManager; this.OnGameInitialized = onGameInitialized; this.OnGameExiting = onGameExiting; Game1.input = new SInputState(); @@ -307,6 +312,7 @@ namespace StardewModdingAPI.Framework try { this.DeprecationManager.PrintQueued(); + this.PerformanceCounterManager.PrintQueued(); /********* ** First-tick initialization @@ -382,7 +388,7 @@ namespace StardewModdingAPI.Framework // state while mods are running their code. This is risky, because data changes can // conflict (e.g. collection changed during enumeration errors) and data may change // unexpectedly from one mod instruction to the next. - // + // // Therefore we can just run Game1.Update here without raising any SMAPI events. There's // a small chance that the task will finish after we defer but before the game checks, // which means technically events should be raised, but the effects of missing one diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index b5e6307a..933590d3 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -11,7 +11,7 @@ using StardewModdingAPI.Framework; using StardewModdingAPI.Toolkit.Utilities; [assembly: InternalsVisibleTo("SMAPI.Tests")] -[assembly: InternalsVisibleTo("SMAPI.Mods.ConsoleCommands")] +[assembly: InternalsVisibleTo("ConsoleCommands")] [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] // Moq for unit testing namespace StardewModdingAPI { -- cgit From 694cca4b21878850ba6131105a0c560fdfbc5f10 Mon Sep 17 00:00:00 2001 From: Drachenkaetzchen Date: Wed, 15 Jan 2020 16:01:35 +0100 Subject: Added documentation for all performance counter methods and members. Refactored the naming of several members and methods to reflect their actual intention. --- .../Commands/Other/PerformanceCounterCommand.cs | 30 ++-- .../Framework/PerformanceCounter/AlertContext.cs | 18 ++- .../Framework/PerformanceCounter/AlertEntry.cs | 25 +++- .../EventPerformanceCounterCollection.cs | 5 + .../PerformanceCounter/IPerformanceCounterEvent.cs | 15 -- .../PerformanceCounter/PerformanceCounter.cs | 99 ++++++------- .../PerformanceCounterCollection.cs | 123 +++++++++------- .../PerformanceCounter/PerformanceCounterEntry.cs | 10 +- .../PerformanceCounterManager.cs | 157 ++++++++++++--------- src/SMAPI/Framework/SCore.cs | 2 +- src/SMAPI/Framework/SGame.cs | 2 +- 11 files changed, 276 insertions(+), 210 deletions(-) delete mode 100644 src/SMAPI/Framework/PerformanceCounter/IPerformanceCounterEvent.cs (limited to 'src/SMAPI.Mods.ConsoleCommands/Framework') diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/PerformanceCounterCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/PerformanceCounterCommand.cs index 84b9504e..750e3792 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/PerformanceCounterCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/PerformanceCounterCommand.cs @@ -134,8 +134,8 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other { if (sourceName == null) { - collection.Monitor = true; - collection.MonitorThresholdMilliseconds = threshold; + collection.EnableAlerts = true; + collection.AlertThresholdMilliseconds = threshold; monitor.Log($"Set up monitor for '{collectionName}' with '{this.FormatMilliseconds(threshold)}'", LogLevel.Info); return; } @@ -145,8 +145,8 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other { if (performanceCounter.Value.Source.ToLowerInvariant().Equals(sourceName.ToLowerInvariant())) { - performanceCounter.Value.Monitor = true; - performanceCounter.Value.MonitorThresholdMilliseconds = threshold; + performanceCounter.Value.EnableAlerts = true; + performanceCounter.Value.AlertThresholdMilliseconds = threshold; monitor.Log($"Set up monitor for '{sourceName}' in collection '{collectionName}' with '{this.FormatMilliseconds(threshold)}", LogLevel.Info); return; } @@ -167,17 +167,17 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other int clearedCounters = 0; foreach (PerformanceCounterCollection collection in SCore.PerformanceCounterManager.PerformanceCounterCollections) { - if (collection.Monitor) + if (collection.EnableAlerts) { - collection.Monitor = false; + collection.EnableAlerts = false; clearedCounters++; } foreach (var performanceCounter in collection.PerformanceCounters) { - if (performanceCounter.Value.Monitor) + if (performanceCounter.Value.EnableAlerts) { - performanceCounter.Value.Monitor = false; + performanceCounter.Value.EnableAlerts = false; clearedCounters++; } } @@ -197,14 +197,14 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other foreach (PerformanceCounterCollection collection in SCore.PerformanceCounterManager.PerformanceCounterCollections) { - if (collection.Monitor) + if (collection.EnableAlerts) { - collectionMonitors.Add((collection.Name, collection.MonitorThresholdMilliseconds)); + collectionMonitors.Add((collection.Name, collection.AlertThresholdMilliseconds)); } sourceMonitors.AddRange(from performanceCounter in - collection.PerformanceCounters where performanceCounter.Value.Monitor - select (collection.Name, performanceCounter.Value.Source, performanceCounter.Value.MonitorThresholdMilliseconds)); + collection.PerformanceCounters where performanceCounter.Value.EnableAlerts + select (collection.Name, performanceCounter.Value.Source, MonitorThresholdMilliseconds: performanceCounter.Value.AlertThresholdMilliseconds)); } if (collectionMonitors.Count > 0) @@ -377,7 +377,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other switch (type) { case "category": - SCore.PerformanceCounterManager.ResetCategory(name); + SCore.PerformanceCounterManager.ResetCollection(name); monitor.Log($"All performance counters for category {name} are now cleared.", LogLevel.Info); break; case "mod": @@ -491,8 +491,8 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other { item.Key, this.FormatMilliseconds(item.Value.GetAverage(averageInterval), thresholdMilliseconds), - this.FormatMilliseconds(item.Value.GetLastEntry()?.Elapsed.TotalMilliseconds), - this.FormatMilliseconds(item.Value.GetPeak()?.Elapsed.TotalMilliseconds) + this.FormatMilliseconds(item.Value.GetLastEntry()?.ElapsedMilliseconds), + this.FormatMilliseconds(item.Value.GetPeak()?.ElapsedMilliseconds) } )); diff --git a/src/SMAPI/Framework/PerformanceCounter/AlertContext.cs b/src/SMAPI/Framework/PerformanceCounter/AlertContext.cs index c4a57a49..63f0a5ed 100644 --- a/src/SMAPI/Framework/PerformanceCounter/AlertContext.cs +++ b/src/SMAPI/Framework/PerformanceCounter/AlertContext.cs @@ -1,14 +1,26 @@ namespace StardewModdingAPI.Framework.PerformanceCounter { - public struct AlertContext + /// The context for an alert. + internal struct AlertContext { - public string Source; - public double Elapsed; + /// The source which triggered the alert. + public readonly string Source; + /// The elapsed milliseconds. + public readonly double Elapsed; + + /// Creates a new alert context. + /// The source which triggered the alert. + /// The elapsed milliseconds. public AlertContext(string source, double elapsed) { this.Source = source; this.Elapsed = elapsed; } + + public override string ToString() + { + return $"{this.Source}: {this.Elapsed:F2}ms"; + } } } diff --git a/src/SMAPI/Framework/PerformanceCounter/AlertEntry.cs b/src/SMAPI/Framework/PerformanceCounter/AlertEntry.cs index 284af1ce..b87d8642 100644 --- a/src/SMAPI/Framework/PerformanceCounter/AlertEntry.cs +++ b/src/SMAPI/Framework/PerformanceCounter/AlertEntry.cs @@ -2,18 +2,31 @@ using System.Collections.Generic; namespace StardewModdingAPI.Framework.PerformanceCounter { + /// A single alert entry. internal struct AlertEntry { - public PerformanceCounterCollection Collection; - public double ExecutionTimeMilliseconds; - public double Threshold; - public List Context; + /// The collection in which the alert occurred. + public readonly PerformanceCounterCollection Collection; - public AlertEntry(PerformanceCounterCollection collection, double executionTimeMilliseconds, double threshold, List context) + /// The actual execution time in milliseconds. + public readonly double ExecutionTimeMilliseconds; + + /// The configured alert threshold. + public readonly double ThresholdMilliseconds; + + /// The context list, which records all sources involved in exceeding the threshold. + public readonly List Context; + + /// Creates a new alert entry. + /// The source collection in which the alert occurred. + /// The actual execution time in milliseconds. + /// The configured threshold in milliseconds. + /// A list of AlertContext to record which sources were involved + public AlertEntry(PerformanceCounterCollection collection, double executionTimeMilliseconds, double thresholdMilliseconds, List context) { this.Collection = collection; this.ExecutionTimeMilliseconds = executionTimeMilliseconds; - this.Threshold = threshold; + this.ThresholdMilliseconds = thresholdMilliseconds; this.Context = context; } } diff --git a/src/SMAPI/Framework/PerformanceCounter/EventPerformanceCounterCollection.cs b/src/SMAPI/Framework/PerformanceCounter/EventPerformanceCounterCollection.cs index 1aec28f3..4690c512 100644 --- a/src/SMAPI/Framework/PerformanceCounter/EventPerformanceCounterCollection.cs +++ b/src/SMAPI/Framework/PerformanceCounter/EventPerformanceCounterCollection.cs @@ -2,8 +2,13 @@ using StardewModdingAPI.Framework.Events; namespace StardewModdingAPI.Framework.PerformanceCounter { + /// Represents a performance counter collection specific to game events. internal class EventPerformanceCounterCollection: PerformanceCounterCollection { + /// Creates a new event performance counter collection. + /// The performance counter manager. + /// The ManagedEvent. + /// If the event is flagged as important. public EventPerformanceCounterCollection(PerformanceCounterManager manager, IManagedEvent @event, bool isImportant) : base(manager, @event.GetName(), isImportant) { } diff --git a/src/SMAPI/Framework/PerformanceCounter/IPerformanceCounterEvent.cs b/src/SMAPI/Framework/PerformanceCounter/IPerformanceCounterEvent.cs deleted file mode 100644 index 1bcf4fa0..00000000 --- a/src/SMAPI/Framework/PerformanceCounter/IPerformanceCounterEvent.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace StardewModdingAPI.Framework.Utilities -{ - public interface IPerformanceCounterEvent - { - string GetEventName(); - long GetAverageCallsPerSecond(); - - double GetGameAverageExecutionTime(); - double GetModsAverageExecutionTime(); - double GetAverageExecutionTime(); - } -} diff --git a/src/SMAPI/Framework/PerformanceCounter/PerformanceCounter.cs b/src/SMAPI/Framework/PerformanceCounter/PerformanceCounter.cs index 3dbc693a..b2ec4c90 100644 --- a/src/SMAPI/Framework/PerformanceCounter/PerformanceCounter.cs +++ b/src/SMAPI/Framework/PerformanceCounter/PerformanceCounter.cs @@ -1,26 +1,32 @@ using System; -using System.Diagnostics; using System.Linq; using Cyotek.Collections.Generic; -using StardewModdingAPI.Framework.Utilities; namespace StardewModdingAPI.Framework.PerformanceCounter { internal class PerformanceCounter { + /// The size of the ring buffer. private const int MAX_ENTRIES = 16384; - public string Source { get; } - public static Stopwatch Stopwatch = new Stopwatch(); - public static long TotalNumEventsLogged; - public double MonitorThresholdMilliseconds { get; set; } - public bool Monitor { get; set; } + /// The collection to which this performance counter belongs. private readonly PerformanceCounterCollection ParentCollection; + /// The circular buffer which stores all performance counter entries private readonly CircularBuffer _counter; + /// The peak execution time private PerformanceCounterEntry? PeakPerformanceCounterEntry; + /// The name of the source. + public string Source { get; } + + /// The alert threshold in milliseconds + public double AlertThresholdMilliseconds { get; set; } + + /// If alerting is enabled or not + public bool EnableAlerts { get; set; } + public PerformanceCounter(PerformanceCounterCollection parentCollection, string source) { this.ParentCollection = parentCollection; @@ -28,90 +34,87 @@ namespace StardewModdingAPI.Framework.PerformanceCounter this._counter = new CircularBuffer(PerformanceCounter.MAX_ENTRIES); } - public void Reset() - { - this._counter.Clear(); - this.PeakPerformanceCounterEntry = null; - } - - public int GetAverageCallsPerSecond() - { - var x = this._counter.GroupBy( - p => - (int) p.EventTime.Subtract( - new DateTime(1970, 1, 1) - ).TotalSeconds); - - return x.Last().Count(); - } - + /// Adds a new performance counter entry to the list. Updates the peak entry and adds an alert if + /// monitoring is enabled and the execution time exceeds the threshold. + /// The entry to add. public void Add(PerformanceCounterEntry entry) { - PerformanceCounter.Stopwatch.Start(); this._counter.Put(entry); - if (this.Monitor && entry.Elapsed.TotalMilliseconds > this.MonitorThresholdMilliseconds) - { - this.ParentCollection.AddAlert(entry.Elapsed.TotalMilliseconds, this.MonitorThresholdMilliseconds, new AlertContext(this.Source, entry.Elapsed.TotalMilliseconds)); - } + if (this.EnableAlerts && entry.ElapsedMilliseconds > this.AlertThresholdMilliseconds) + this.ParentCollection.AddAlert(entry.ElapsedMilliseconds, this.AlertThresholdMilliseconds, + new AlertContext(this.Source, entry.ElapsedMilliseconds)); if (this.PeakPerformanceCounterEntry == null) - { this.PeakPerformanceCounterEntry = entry; - } else { - if (entry.Elapsed.TotalMilliseconds > this.PeakPerformanceCounterEntry.Value.Elapsed.TotalMilliseconds) - { + if (entry.ElapsedMilliseconds > this.PeakPerformanceCounterEntry.Value.ElapsedMilliseconds) this.PeakPerformanceCounterEntry = entry; - } } + } - PerformanceCounter.Stopwatch.Stop(); - PerformanceCounter.TotalNumEventsLogged++; + /// Clears all performance counter entries and resets the peak entry. + public void Reset() + { + this._counter.Clear(); + this.PeakPerformanceCounterEntry = null; } + /// Returns the peak entry. + /// The peak entry. public PerformanceCounterEntry? GetPeak() { return this.PeakPerformanceCounterEntry; } + /// Resets the peak entry. public void ResetPeak() { this.PeakPerformanceCounterEntry = null; } + /// Returns the last entry added to the list. + /// The last entry public PerformanceCounterEntry? GetLastEntry() { if (this._counter.IsEmpty) - { return null; - } + return this._counter.PeekLast(); } + /// Returns the average execution time of all entries. + /// The average execution time in milliseconds. public double GetAverage() { if (this._counter.IsEmpty) - { return 0; - } - return this._counter.Average(p => p.Elapsed.TotalMilliseconds); + return this._counter.Average(p => p.ElapsedMilliseconds); } - public double GetAverage(TimeSpan range) + /// Returns the average over a given time span. + /// The time range to retrieve. + /// The DateTime from which to start the average. Defaults to DateTime.UtcNow if null + /// The average execution time in milliseconds. + /// + /// The relativeTo parameter specifies from which point in time the range is subtracted. Example: + /// If DateTime is set to 60 seconds ago, and the range is set to 60 seconds, the method would return + /// the average between all entries between 120s ago and 60s ago. + /// + public double GetAverage(TimeSpan range, DateTime? relativeTo = null) { if (this._counter.IsEmpty) - { return 0; - } - var lastTime = this._counter.Max(x => x.EventTime); - var start = lastTime.Subtract(range); + if (relativeTo == null) + relativeTo = DateTime.UtcNow; + + DateTime start = relativeTo.Value.Subtract(range); - var entries = this._counter.Where(x => (x.EventTime >= start) && (x.EventTime <= lastTime)); - return entries.Average(x => x.Elapsed.TotalMilliseconds); + var entries = this._counter.Where(x => (x.EventTime >= start) && (x.EventTime <= relativeTo)); + return entries.Average(x => x.ElapsedMilliseconds); } } } diff --git a/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterCollection.cs b/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterCollection.cs index 343fddf6..b48efd67 100644 --- a/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterCollection.cs +++ b/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterCollection.cs @@ -2,23 +2,41 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; -using StardewModdingAPI.Framework.Utilities; namespace StardewModdingAPI.Framework.PerformanceCounter { internal class PerformanceCounterCollection { - public IDictionary PerformanceCounters { get; } = new Dictionary(); - private DateTime StartDateTime = DateTime.Now; - private long CallCount; - public string Name { get; private set; } - public bool IsImportant { get; set; } - private readonly Stopwatch Stopwatch = new Stopwatch(); - private readonly PerformanceCounterManager PerformanceCounterManager; - public double MonitorThresholdMilliseconds { get; set; } - public bool Monitor { get; set; } + /// The list of triggered performance counters. private readonly List TriggeredPerformanceCounters = new List(); + /// The stopwatch used to track the invocation time. + private readonly Stopwatch InvocationStopwatch = new Stopwatch(); + + /// The performance counter manager. + private readonly PerformanceCounterManager PerformanceCounterManager; + + /// Holds the time to calculate the average calls per second. + private DateTime CallsPerSecondStart = DateTime.UtcNow; + + /// The number of invocations of this collection. + private long CallCount; + + public IDictionary PerformanceCounters { get; } = new Dictionary(); + + /// The name of this collection. + public string Name { get; } + + /// Flag if this collection is important (used for the console summary command). + public bool IsImportant { get; } + + /// The alert threshold in milliseconds. + public double AlertThresholdMilliseconds { get; set; } + + /// If alerting is enabled or not + public bool EnableAlerts { get; set; } + + public PerformanceCounterCollection(PerformanceCounterManager performanceCounterManager, string name, bool isImportant) { this.Name = name; @@ -32,111 +50,120 @@ namespace StardewModdingAPI.Framework.PerformanceCounter this.Name = name; } + /// Tracks a single invocation for a named source. + /// The name of the source. + /// The entry. public void Track(string source, PerformanceCounterEntry entry) { if (!this.PerformanceCounters.ContainsKey(source)) - { this.PerformanceCounters.Add(source, new PerformanceCounter(this, source)); - } + this.PerformanceCounters[source].Add(entry); - if (this.Monitor) - { - this.TriggeredPerformanceCounters.Add(new AlertContext(source, entry.Elapsed.TotalMilliseconds)); - } + if (this.EnableAlerts) + this.TriggeredPerformanceCounters.Add(new AlertContext(source, entry.ElapsedMilliseconds)); } + /// Returns the average execution time for all non-game internal sources. + /// The average execution time in milliseconds public double GetModsAverageExecutionTime() { - return this.PerformanceCounters.Where(p => p.Key != Constants.GamePerformanceCounterName).Sum(p => p.Value.GetAverage()); + return this.PerformanceCounters.Where(p => + p.Key != Constants.GamePerformanceCounterName).Sum(p => p.Value.GetAverage()); } + /// Returns the overall average execution time. + /// The average execution time in milliseconds public double GetAverageExecutionTime() { return this.PerformanceCounters.Sum(p => p.Value.GetAverage()); } + /// Returns the average execution time for game-internal sources. + /// The average execution time in milliseconds public double GetGameAverageExecutionTime() { if (this.PerformanceCounters.TryGetValue(Constants.GamePerformanceCounterName, out PerformanceCounter gameExecTime)) - { return gameExecTime.GetAverage(); - } return 0; } + /// Begins tracking the invocation of this collection. public void BeginTrackInvocation() { - if (this.Monitor) + if (this.EnableAlerts) { this.TriggeredPerformanceCounters.Clear(); - this.Stopwatch.Reset(); - this.Stopwatch.Start(); + this.InvocationStopwatch.Reset(); + this.InvocationStopwatch.Start(); } this.CallCount++; - } + /// Ends tracking the invocation of this collection. Also records an alert if alerting is enabled + /// and the invocation time exceeds the threshold. public void EndTrackInvocation() { - if (!this.Monitor) return; + if (!this.EnableAlerts) return; - this.Stopwatch.Stop(); - if (this.Stopwatch.Elapsed.TotalMilliseconds >= this.MonitorThresholdMilliseconds) - { - this.AddAlert(this.Stopwatch.Elapsed.TotalMilliseconds, - this.MonitorThresholdMilliseconds, this.TriggeredPerformanceCounters); - } + this.InvocationStopwatch.Stop(); + + if (this.InvocationStopwatch.Elapsed.TotalMilliseconds >= this.AlertThresholdMilliseconds) + this.AddAlert(this.InvocationStopwatch.Elapsed.TotalMilliseconds, + this.AlertThresholdMilliseconds, this.TriggeredPerformanceCounters); } - public void AddAlert(double executionTimeMilliseconds, double threshold, List alerts) + /// Adds an alert. + /// The execution time in milliseconds. + /// The configured threshold. + /// The list of alert contexts. + public void AddAlert(double executionTimeMilliseconds, double thresholdMilliseconds, List alerts) { this.PerformanceCounterManager.AddAlert(new AlertEntry(this, executionTimeMilliseconds, - threshold, alerts)); + thresholdMilliseconds, alerts)); } - public void AddAlert(double executionTimeMilliseconds, double threshold, AlertContext alert) + /// Adds an alert for a single AlertContext + /// The execution time in milliseconds. + /// The configured threshold. + /// The context + public void AddAlert(double executionTimeMilliseconds, double thresholdMilliseconds, AlertContext alert) { - this.AddAlert(executionTimeMilliseconds, threshold, new List() {alert}); + this.AddAlert(executionTimeMilliseconds, thresholdMilliseconds, new List() {alert}); } + /// Resets the calls per second counter. public void ResetCallsPerSecond() { this.CallCount = 0; - this.StartDateTime = DateTime.Now; + this.CallsPerSecondStart = DateTime.UtcNow; } + /// Resets all performance counters in this collection. public void Reset() { foreach (var i in this.PerformanceCounters) - { i.Value.Reset(); - i.Value.ResetPeak(); - } } + /// Resets the performance counter for a specific source. + /// The source name public void ResetSource(string source) { foreach (var i in this.PerformanceCounters) - { if (i.Value.Source.Equals(source, StringComparison.InvariantCultureIgnoreCase)) - { i.Value.Reset(); - i.Value.ResetPeak(); - } - } } + /// Returns the average calls per second. + /// The average calls per second. public long GetAverageCallsPerSecond() { - long runtimeInSeconds = (long) DateTime.Now.Subtract(this.StartDateTime).TotalSeconds; + long runtimeInSeconds = (long) DateTime.UtcNow.Subtract(this.CallsPerSecondStart).TotalSeconds; - if (runtimeInSeconds == 0) - { - return 0; - } + if (runtimeInSeconds == 0) return 0; return this.CallCount / runtimeInSeconds; } diff --git a/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterEntry.cs b/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterEntry.cs index 8e156a32..a50fce7d 100644 --- a/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterEntry.cs +++ b/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterEntry.cs @@ -1,10 +1,14 @@ using System; -namespace StardewModdingAPI.Framework.Utilities +namespace StardewModdingAPI.Framework.PerformanceCounter { - public struct PerformanceCounterEntry + /// A single performance counter entry. Records the DateTime of the event and the elapsed millisecond. + internal struct PerformanceCounterEntry { + /// The DateTime when the entry occured. public DateTime EventTime; - public TimeSpan Elapsed; + + /// The elapsed milliseconds + public double ElapsedMilliseconds; } } diff --git a/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterManager.cs b/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterManager.cs index 9e77e2fa..d8f1f172 100644 --- a/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterManager.cs +++ b/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterManager.cs @@ -4,72 +4,62 @@ using System.Diagnostics; using System.Linq; using System.Text; using StardewModdingAPI.Framework.Events; -using StardewModdingAPI.Framework.Utilities; namespace StardewModdingAPI.Framework.PerformanceCounter { internal class PerformanceCounterManager { public HashSet PerformanceCounterCollections = new HashSet(); - public List Alerts = new List(); + + /// The recorded alerts. + private readonly List Alerts = new List(); + + /// The monitor for output logging. private readonly IMonitor Monitor; - private readonly Stopwatch Stopwatch = new Stopwatch(); + /// The invocation stopwatch. + private readonly Stopwatch InvocationStopwatch = new Stopwatch(); + + /// Constructs a performance counter manager. + /// The monitor for output logging. public PerformanceCounterManager(IMonitor monitor) { this.Monitor = monitor; } + /// Resets all performance counters in all collections. public void Reset() { - foreach (var performanceCounter in this.PerformanceCounterCollections) - { - foreach (var eventPerformanceCounter in performanceCounter.PerformanceCounters) - { - eventPerformanceCounter.Value.Reset(); - } - } - } - - /// Print any queued messages. - public void PrintQueued() - { - if (this.Alerts.Count == 0) + foreach (var eventPerformanceCounter in + this.PerformanceCounterCollections.SelectMany(performanceCounter => performanceCounter.PerformanceCounters)) { - return; + eventPerformanceCounter.Value.Reset(); } - StringBuilder sb = new StringBuilder(); - - foreach (var alert in this.Alerts) - { - sb.AppendLine($"{alert.Collection.Name} took {alert.ExecutionTimeMilliseconds:F2}ms (exceeded threshold of {alert.Threshold:F2}ms)"); - - foreach (var context in alert.Context) - { - sb.AppendLine($"{context.Source}: {context.Elapsed:F2}ms"); - } - } - - this.Alerts.Clear(); - - this.Monitor.Log(sb.ToString(), LogLevel.Error); } + /// Begins tracking the invocation for a collection. + /// The collection name public void BeginTrackInvocation(string collectionName) { this.GetOrCreateCollectionByName(collectionName).BeginTrackInvocation(); } + /// Ends tracking the invocation for a collection. + /// public void EndTrackInvocation(string collectionName) { this.GetOrCreateCollectionByName(collectionName).EndTrackInvocation(); } - public void Track(string collectionName, string modName, Action action) + /// Tracks a single performance counter invocation in a specific collection. + /// The name of the collection. + /// The name of the source. + /// The action to execute and track invocation time for. + public void Track(string collectionName, string sourceName, Action action) { DateTime eventTime = DateTime.UtcNow; - this.Stopwatch.Reset(); - this.Stopwatch.Start(); + this.InvocationStopwatch.Reset(); + this.InvocationStopwatch.Start(); try { @@ -77,75 +67,102 @@ namespace StardewModdingAPI.Framework.PerformanceCounter } finally { - this.Stopwatch.Stop(); + this.InvocationStopwatch.Stop(); - this.GetOrCreateCollectionByName(collectionName).Track(modName, new PerformanceCounterEntry + this.GetOrCreateCollectionByName(collectionName).Track(sourceName, new PerformanceCounterEntry { EventTime = eventTime, - Elapsed = this.Stopwatch.Elapsed + ElapsedMilliseconds = this.InvocationStopwatch.Elapsed.TotalMilliseconds }); } } - public PerformanceCounterCollection GetCollectionByName(string name) + /// Gets a collection by name. + /// The name of the collection. + /// The collection or null if none was found. + private PerformanceCounterCollection GetCollectionByName(string name) { return this.PerformanceCounterCollections.FirstOrDefault(collection => collection.Name == name); } - public PerformanceCounterCollection GetOrCreateCollectionByName(string name) + /// Gets a collection by name and creates it if it doesn't exist. + /// The name of the collection. + /// The collection. + private PerformanceCounterCollection GetOrCreateCollectionByName(string name) { PerformanceCounterCollection collection = this.GetCollectionByName(name); - if (collection == null) - { - collection = new PerformanceCounterCollection(this, name); - this.PerformanceCounterCollections.Add(collection); - } + if (collection != null) return collection; + + collection = new PerformanceCounterCollection(this, name); + this.PerformanceCounterCollections.Add(collection); return collection; } - public void ResetCategory(string name) + /// Resets the performance counters for a specific collection. + /// The collection name. + public void ResetCollection(string name) { - foreach (var performanceCounterCollection in this.PerformanceCounterCollections) + foreach (PerformanceCounterCollection performanceCounterCollection in + this.PerformanceCounterCollections.Where(performanceCounterCollection => + performanceCounterCollection.Name.Equals(name, StringComparison.InvariantCultureIgnoreCase))) { - if (performanceCounterCollection.Name.Equals(name, StringComparison.InvariantCultureIgnoreCase)) - { - performanceCounterCollection.ResetCallsPerSecond(); - performanceCounterCollection.Reset(); - } + performanceCounterCollection.ResetCallsPerSecond(); + performanceCounterCollection.Reset(); } } + /// Resets performance counters for a specific source. + /// The name of the source. public void ResetSource(string name) { - foreach (var performanceCounterCollection in this.PerformanceCounterCollections) - { + foreach (PerformanceCounterCollection performanceCounterCollection in this.PerformanceCounterCollections) performanceCounterCollection.ResetSource(name); - } } + /// Print any queued alerts. + public void PrintQueuedAlerts() + { + if (this.Alerts.Count == 0) return; + + StringBuilder sb = new StringBuilder(); + + foreach (AlertEntry alert in this.Alerts) + { + sb.AppendLine($"{alert.Collection.Name} took {alert.ExecutionTimeMilliseconds:F2}ms (exceeded threshold of {alert.ThresholdMilliseconds:F2}ms)"); + foreach (AlertContext context in alert.Context.OrderByDescending(p => p.Elapsed)) + sb.AppendLine(context.ToString()); + } + + this.Alerts.Clear(); + this.Monitor.Log(sb.ToString(), LogLevel.Error); + } + + /// Adds an alert to the queue. + /// The alert to add. public void AddAlert(AlertEntry entry) { this.Alerts.Add(entry); } - public void InitializePerformanceCounterEvents(EventManager eventManager) + /// Initialized the default performance counter collections. + /// The event manager. + public void InitializePerformanceCounterCollections(EventManager eventManager) { this.PerformanceCounterCollections = new HashSet() { new EventPerformanceCounterCollection(this, eventManager.MenuChanged, false), - // Rendering Events - new EventPerformanceCounterCollection(this, eventManager.Rendering, true), + new EventPerformanceCounterCollection(this, eventManager.Rendering, false), new EventPerformanceCounterCollection(this, eventManager.Rendered, true), - new EventPerformanceCounterCollection(this, eventManager.RenderingWorld, true), + new EventPerformanceCounterCollection(this, eventManager.RenderingWorld, false), new EventPerformanceCounterCollection(this, eventManager.RenderedWorld, true), - new EventPerformanceCounterCollection(this, eventManager.RenderingActiveMenu, true), + new EventPerformanceCounterCollection(this, eventManager.RenderingActiveMenu, false), new EventPerformanceCounterCollection(this, eventManager.RenderedActiveMenu, true), - new EventPerformanceCounterCollection(this, eventManager.RenderingHud, true), + new EventPerformanceCounterCollection(this, eventManager.RenderingHud, false), new EventPerformanceCounterCollection(this, eventManager.RenderedHud, true), new EventPerformanceCounterCollection(this, eventManager.WindowResized, false), @@ -172,19 +189,19 @@ namespace StardewModdingAPI.Framework.PerformanceCounter new EventPerformanceCounterCollection(this, eventManager.CursorMoved, true), new EventPerformanceCounterCollection(this, eventManager.MouseWheelScrolled, true), - new EventPerformanceCounterCollection(this, eventManager.PeerContextReceived, true), - new EventPerformanceCounterCollection(this, eventManager.ModMessageReceived, true), - new EventPerformanceCounterCollection(this, eventManager.PeerDisconnected, true), + new EventPerformanceCounterCollection(this, eventManager.PeerContextReceived, false), + new EventPerformanceCounterCollection(this, eventManager.ModMessageReceived, false), + new EventPerformanceCounterCollection(this, eventManager.PeerDisconnected, false), new EventPerformanceCounterCollection(this, eventManager.InventoryChanged, true), - new EventPerformanceCounterCollection(this, eventManager.LevelChanged, true), - new EventPerformanceCounterCollection(this, eventManager.Warped, true), + new EventPerformanceCounterCollection(this, eventManager.LevelChanged, false), + new EventPerformanceCounterCollection(this, eventManager.Warped, false), - new EventPerformanceCounterCollection(this, eventManager.LocationListChanged, true), - new EventPerformanceCounterCollection(this, eventManager.BuildingListChanged, true), - new EventPerformanceCounterCollection(this, eventManager.LocationListChanged, true), + new EventPerformanceCounterCollection(this, eventManager.LocationListChanged, false), + new EventPerformanceCounterCollection(this, eventManager.BuildingListChanged, false), + new EventPerformanceCounterCollection(this, eventManager.LocationListChanged, false), new EventPerformanceCounterCollection(this, eventManager.DebrisListChanged, true), new EventPerformanceCounterCollection(this, eventManager.LargeTerrainFeatureListChanged, true), - new EventPerformanceCounterCollection(this, eventManager.NpcListChanged, true), + new EventPerformanceCounterCollection(this, eventManager.NpcListChanged, false), new EventPerformanceCounterCollection(this, eventManager.ObjectListChanged, true), new EventPerformanceCounterCollection(this, eventManager.ChestInventoryChanged, true), new EventPerformanceCounterCollection(this, eventManager.TerrainFeatureListChanged, true), diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index 5b0c6691..af7513e3 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -169,7 +169,7 @@ namespace StardewModdingAPI.Framework SCore.PerformanceCounterManager = new PerformanceCounterManager(this.Monitor); this.EventManager = new EventManager(this.Monitor, this.ModRegistry, SCore.PerformanceCounterManager); - SCore.PerformanceCounterManager.InitializePerformanceCounterEvents(this.EventManager); + SCore.PerformanceCounterManager.InitializePerformanceCounterCollections(this.EventManager); SCore.DeprecationManager = new DeprecationManager(this.Monitor, this.ModRegistry); diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index 8aba9b57..266e2e6f 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -312,7 +312,7 @@ namespace StardewModdingAPI.Framework try { this.DeprecationManager.PrintQueued(); - this.PerformanceCounterManager.PrintQueued(); + this.PerformanceCounterManager.PrintQueuedAlerts(); /********* ** First-tick initialization -- cgit From 1d58a525fa170a8e0de3de38477c501fb83f0b5a Mon Sep 17 00:00:00 2001 From: Drachenkaetzchen Date: Wed, 15 Jan 2020 17:42:46 +0100 Subject: Added optional right-align for the table output --- .../Framework/Commands/TrainerCommand.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) (limited to 'src/SMAPI.Mods.ConsoleCommands/Framework') diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/TrainerCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/TrainerCommand.cs index 466b8f6e..8f0d89ba 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/TrainerCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/TrainerCommand.cs @@ -66,7 +66,8 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands /// The data to display. /// The table header. /// Returns a set of fields for a data value. - protected string GetTableString(IEnumerable data, string[] header, Func getRow) + /// True to right-align the data, false for left-align. Default false. + protected string GetTableString(IEnumerable data, string[] header, Func getRow, bool rightAlign = false) { // get table data int[] widths = header.Select(p => p.Length).ToArray(); @@ -92,6 +93,15 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands }; lines.AddRange(rows); + if (rightAlign) + { + return string.Join( + Environment.NewLine, + lines.Select(line => string.Join(" | ", line.Select((field, i) => field.PadLeft(widths[i], ' ')).ToArray()) + ) + ); + } + return string.Join( Environment.NewLine, lines.Select(line => string.Join(" | ", line.Select((field, i) => field.PadRight(widths[i], ' ')).ToArray()) -- cgit From fce5814bcb150c4ff105a37dfcd57f397b117e48 Mon Sep 17 00:00:00 2001 From: Drachenkaetzchen Date: Wed, 15 Jan 2020 17:43:41 +0100 Subject: Added documentation for all commands. Renamed the "monitor" command to "trigger". Method name refactoring to be more consistent. --- .../Commands/Other/PerformanceCounterCommand.cs | 596 ++++++++++++--------- src/SMAPI/Framework/Events/EventManager.cs | 1 + src/SMAPI/Framework/Events/ManagedEvent.cs | 1 + .../PerformanceCounter/PerformanceCounter.cs | 4 + .../PerformanceCounterManager.cs | 6 +- 5 files changed, 361 insertions(+), 247 deletions(-) (limited to 'src/SMAPI.Mods.ConsoleCommands/Framework') diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/PerformanceCounterCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/PerformanceCounterCommand.cs index 750e3792..82b44562 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/PerformanceCounterCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/PerformanceCounterCommand.cs @@ -7,65 +7,71 @@ using StardewModdingAPI.Framework.PerformanceCounter; namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other { + // ReSharper disable once UnusedType.Global internal class PerformanceCounterCommand : TrainerCommand { - private readonly Dictionary CommandNames = new Dictionary() + /// The command names and aliases + private readonly Dictionary SubCommandNames = new Dictionary() { - {Command.Summary, new[] {"summary", "sum", "s"}}, - {Command.Detail, new[] {"detail", "d"}}, - {Command.Reset, new[] {"reset", "r"}}, - {Command.Monitor, new[] {"monitor"}}, - {Command.Examples, new[] {"examples"}}, - {Command.Concepts, new[] {"concepts"}}, - {Command.Help, new[] {"help"}}, + {SubCommand.Summary, new[] {"summary", "sum", "s"}}, + {SubCommand.Detail, new[] {"detail", "d"}}, + {SubCommand.Reset, new[] {"reset", "r"}}, + {SubCommand.Trigger, new[] {"trigger"}}, + {SubCommand.Examples, new[] {"examples"}}, + {SubCommand.Concepts, new[] {"concepts"}}, + {SubCommand.Help, new[] {"help"}}, }; - private enum Command + /// The available commands enum + private enum SubCommand { Summary, Detail, Reset, - Monitor, + Trigger, Examples, Help, Concepts, None } + /// Construct an instance. public PerformanceCounterCommand() : base("pc", PerformanceCounterCommand.GetDescription()) { } + /// Handle the command. + /// Writes messages to the console and log file. + /// The command name. + /// The command arguments. public override void Handle(IMonitor monitor, string command, ArgumentParser args) { if (args.TryGet(0, "command", out string subCommandString, false)) { - Command subCommand = this.ParseCommandString(subCommandString); + SubCommand subSubCommand = this.ParseCommandString(subCommandString); - switch (subCommand) + switch (subSubCommand) { - case Command.Summary: - this.DisplayPerformanceCounterSummary(monitor, args); + case SubCommand.Summary: + this.HandleSummarySubCommand(monitor, args); break; - case Command.Detail: - this.DisplayPerformanceCounterDetail(monitor, args); + case SubCommand.Detail: + this.HandleDetailSubCommand(monitor, args); break; - case Command.Reset: - this.ResetCounter(monitor, args); + case SubCommand.Reset: + this.HandleResetSubCommand(monitor, args); break; - case Command.Monitor: - this.HandleMonitor(monitor, args); + case SubCommand.Trigger: + this.HandleTriggerSubCommand(monitor, args); break; - case Command.Examples: + case SubCommand.Examples: break; - case Command.Concepts: - this.ShowHelp(monitor, Command.Concepts); + case SubCommand.Concepts: + this.OutputHelp(monitor, SubCommand.Concepts); break; - case Command.Help: - args.TryGet(1, "command", out string commandString, true); - - var helpCommand = this.ParseCommandString(commandString); - this.ShowHelp(monitor, helpCommand); + case SubCommand.Help: + if (args.TryGet(1, "command", out string commandString)) + this.OutputHelp(monitor, this.ParseCommandString(commandString)); break; default: this.LogUsageError(monitor, $"Unknown command {subCommandString}"); @@ -73,60 +79,154 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other } } else + this.HandleSummarySubCommand(monitor, args); + } + + /// Handles the summary sub command. + /// Writes messages to the console and log file. + /// The command arguments. + private void HandleSummarySubCommand(IMonitor monitor, ArgumentParser args) + { + IEnumerable data; + + if (!args.TryGet(1, "mode", out string mode, false)) + { + mode = "important"; + } + + switch (mode) + { + case null: + case "important": + data = SCore.PerformanceCounterManager.PerformanceCounterCollections.Where(p => p.IsImportant); + break; + case "all": + data = SCore.PerformanceCounterManager.PerformanceCounterCollections; + break; + default: + data = SCore.PerformanceCounterManager.PerformanceCounterCollections.Where(p => + p.Name.ToLowerInvariant().Contains(mode.ToLowerInvariant())); + break; + } + + double? threshold = null; + + if (args.TryGetDecimal(2, "threshold", out decimal t, false)) { - this.DisplayPerformanceCounterSummary(monitor, args); + threshold = (double?) t; } + + StringBuilder sb = new StringBuilder(); + + sb.AppendLine("Summary:"); + sb.AppendLine(this.GetTableString( + data: data, + header: new[] {"Collection", "Avg Calls/s", "Avg Execution Time (Game)", "Avg Execution Time (Mods)", "Avg Execution Time (Game+Mods)"}, + getRow: item => new[] + { + item.Name, + item.GetAverageCallsPerSecond().ToString(), + this.FormatMilliseconds(item.GetGameAverageExecutionTime(), threshold), + this.FormatMilliseconds(item.GetModsAverageExecutionTime(), threshold), + this.FormatMilliseconds(item.GetAverageExecutionTime(), threshold) + }, + true + )); + + monitor.Log(sb.ToString(), LogLevel.Info); } - private Command ParseCommandString(string command) + /// Handles the detail sub command. + /// Writes messages to the console and log file. + /// The command arguments. + private void HandleDetailSubCommand(IMonitor monitor, ArgumentParser args) { - foreach (var i in this.CommandNames.Where(i => i.Value.Any(str => str.Equals(command, StringComparison.InvariantCultureIgnoreCase)))) + var collections = new List(); + TimeSpan averageInterval = TimeSpan.FromSeconds(60); + double? thresholdMilliseconds = null; + string sourceFilter = null; + + if (args.TryGet(1, "collection", out string collectionName)) { - return i.Key; + collections.AddRange(SCore.PerformanceCounterManager.PerformanceCounterCollections.Where( + collection => collection.Name.ToLowerInvariant().Contains(collectionName.ToLowerInvariant()))); + + if (args.IsDecimal(2) && args.TryGetDecimal(2, "threshold", out decimal value, false)) + { + thresholdMilliseconds = (double?) value; + } + else + { + if (args.TryGet(2, "source", out string sourceName, false)) + { + sourceFilter = sourceName; + } + } } - return Command.None; + foreach (PerformanceCounterCollection c in collections) + { + this.OutputPerformanceCollectionDetail(monitor, c, averageInterval, thresholdMilliseconds, sourceFilter); + } } - private void HandleMonitor(IMonitor monitor, ArgumentParser args) + /// Handles the trigger sub command. + /// Writes messages to the console and log file. + /// The command arguments. + private void HandleTriggerSubCommand(IMonitor monitor, ArgumentParser args) { if (args.TryGet(1, "mode", out string mode, false)) { switch (mode) { case "list": - this.ListMonitors(monitor); + this.OutputAlertTriggers(monitor); break; case "collection": - args.TryGet(2, "name", out string collectionName); - decimal threshold = 0; - if (args.IsDecimal(3) && args.TryGetDecimal(3, "threshold", out threshold, false)) + if (args.TryGet(2, "name", out string collectionName)) { - this.SetCollectionMonitor(monitor, collectionName, null, (double)threshold); - } else if (args.TryGet(3, "source", out string source)) - { - if (args.TryGetDecimal(4, "threshold", out threshold)) + if (args.TryGetDecimal(3, "threshold", out decimal threshold)) { - this.SetCollectionMonitor(monitor, collectionName, source, (double) threshold); + if (args.TryGet(4, "source", out string source, false)) + { + this.ConfigureAlertTrigger(monitor, collectionName, source, threshold); + } + else + { + this.ConfigureAlertTrigger(monitor, collectionName, null, threshold); + } } } break; + case "pause": + SCore.PerformanceCounterManager.PauseAlerts = true; + monitor.Log($"Alerts are now paused.", LogLevel.Info); + break; + case "resume": + SCore.PerformanceCounterManager.PauseAlerts = false; + monitor.Log($"Alerts are now resumed.", LogLevel.Info); + break; case "clear": - this.ClearMonitors(monitor); + this.ClearAlertTriggers(monitor); break; default: - monitor.Log($"Unknown mode {mode}. See 'pc help monitor' for usage."); + this.LogUsageError(monitor, $"Unknown mode {mode}. See 'pc help trigger' for usage."); break; } } else { - this.ListMonitors(monitor); + this.OutputAlertTriggers(monitor); } } - private void SetCollectionMonitor(IMonitor monitor, string collectionName, string sourceName, double threshold) + /// Sets up an an alert trigger. + /// Writes messages to the console and log file. + /// The name of the collection. + /// The name of the source, or null for all sources. + /// The trigger threshold, or 0 to remove. + private void ConfigureAlertTrigger(IMonitor monitor, string collectionName, string sourceName, decimal threshold) { foreach (PerformanceCounterCollection collection in SCore.PerformanceCounterManager.PerformanceCounterCollections) { @@ -134,9 +234,18 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other { if (sourceName == null) { - collection.EnableAlerts = true; - collection.AlertThresholdMilliseconds = threshold; - monitor.Log($"Set up monitor for '{collectionName}' with '{this.FormatMilliseconds(threshold)}'", LogLevel.Info); + if (threshold != 0) + { + collection.EnableAlerts = true; + collection.AlertThresholdMilliseconds = (double) threshold; + monitor.Log($"Set up alert triggering for '{collectionName}' with '{this.FormatMilliseconds((double?) threshold)}'", LogLevel.Info); + } + else + { + collection.EnableAlerts = false; + monitor.Log($"Cleared alert triggering for '{collection}'."); + } + return; } else @@ -145,9 +254,17 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other { if (performanceCounter.Value.Source.ToLowerInvariant().Equals(sourceName.ToLowerInvariant())) { - performanceCounter.Value.EnableAlerts = true; - performanceCounter.Value.AlertThresholdMilliseconds = threshold; - monitor.Log($"Set up monitor for '{sourceName}' in collection '{collectionName}' with '{this.FormatMilliseconds(threshold)}", LogLevel.Info); + if (threshold != 0) + { + performanceCounter.Value.EnableAlerts = true; + performanceCounter.Value.AlertThresholdMilliseconds = (double) threshold; + monitor.Log($"Set up alert triggering for '{sourceName}' in collection '{collectionName}' with '{this.FormatMilliseconds((double?) threshold)}", LogLevel.Info); + } + else + { + performanceCounter.Value.EnableAlerts = false; + } + return; } } @@ -162,15 +279,17 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other } - private void ClearMonitors(IMonitor monitor) + /// Clears alert triggering for all collections. + /// Writes messages to the console and log file. + private void ClearAlertTriggers(IMonitor monitor) { - int clearedCounters = 0; + int clearedTriggers = 0; foreach (PerformanceCounterCollection collection in SCore.PerformanceCounterManager.PerformanceCounterCollections) { if (collection.EnableAlerts) { collection.EnableAlerts = false; - clearedCounters++; + clearedTriggers++; } foreach (var performanceCounter in collection.PerformanceCounters) @@ -178,82 +297,203 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other if (performanceCounter.Value.EnableAlerts) { performanceCounter.Value.EnableAlerts = false; - clearedCounters++; + clearedTriggers++; } } } - monitor.Log($"Cleared {clearedCounters} counters.", LogLevel.Info); + monitor.Log($"Cleared {clearedTriggers} alert triggers.", LogLevel.Info); } - private void ListMonitors(IMonitor monitor) + /// Lists all configured alert triggers. + /// Writes messages to the console and log file. + private void OutputAlertTriggers(IMonitor monitor) { StringBuilder sb = new StringBuilder(); + sb.AppendLine("Configured triggers:"); sb.AppendLine(); - sb.AppendLine(); - var collectionMonitors = new List<(string collectionName, double threshold)>(); - var sourceMonitors = new List<(string collectionName, string sourceName, double threshold)>(); + var collectionTriggers = new List<(string collectionName, double threshold)>(); + var sourceTriggers = new List<(string collectionName, string sourceName, double threshold)>(); foreach (PerformanceCounterCollection collection in SCore.PerformanceCounterManager.PerformanceCounterCollections) { if (collection.EnableAlerts) { - collectionMonitors.Add((collection.Name, collection.AlertThresholdMilliseconds)); + collectionTriggers.Add((collection.Name, collection.AlertThresholdMilliseconds)); } - sourceMonitors.AddRange(from performanceCounter in + sourceTriggers.AddRange(from performanceCounter in collection.PerformanceCounters where performanceCounter.Value.EnableAlerts - select (collection.Name, performanceCounter.Value.Source, MonitorThresholdMilliseconds: performanceCounter.Value.AlertThresholdMilliseconds)); + select (collection.Name, performanceCounter.Value.Source, performanceCounter.Value.AlertThresholdMilliseconds)); } - if (collectionMonitors.Count > 0) + if (collectionTriggers.Count > 0) { - sb.AppendLine("Collection Monitors:"); + sb.AppendLine("Collection Triggers:"); sb.AppendLine(); sb.AppendLine(this.GetTableString( - data: collectionMonitors, + data: collectionTriggers, header: new[] {"Collection", "Threshold"}, getRow: item => new[] { item.collectionName, this.FormatMilliseconds(item.threshold) - } + }, + true )); sb.AppendLine(); - - + } + else + { + sb.AppendLine("No collection triggers."); } - if (sourceMonitors.Count > 0) + if (sourceTriggers.Count > 0) { - sb.AppendLine("Source Monitors:"); + sb.AppendLine("Source Triggers:"); sb.AppendLine(); sb.AppendLine(this.GetTableString( - data: sourceMonitors, + data: sourceTriggers, header: new[] {"Collection", "Source", "Threshold"}, getRow: item => new[] { item.collectionName, item.sourceName, this.FormatMilliseconds(item.threshold) - } + }, + true )); sb.AppendLine(); } + else + { + sb.AppendLine("No source triggers."); + } monitor.Log(sb.ToString(), LogLevel.Info); } - private void ShowHelp(IMonitor monitor, Command command) + /// Handles the reset sub command. + /// Writes messages to the console and log file. + /// The command arguments. + private void HandleResetSubCommand(IMonitor monitor, ArgumentParser args) + { + if (args.TryGet(1, "type", out string type, false, new []{"category", "source"})) + { + args.TryGet(2, "name", out string name); + + switch (type) + { + case "category": + SCore.PerformanceCounterManager.ResetCollection(name); + monitor.Log($"All performance counters for category {name} are now cleared.", LogLevel.Info); + break; + case "source": + SCore.PerformanceCounterManager.ResetSource(name); + monitor.Log($"All performance counters for source {name} are now cleared.", LogLevel.Info); + break; + } + } + else + { + SCore.PerformanceCounterManager.Reset(); + monitor.Log("All performance counters are now cleared.", LogLevel.Info); + } + } + + + /// Outputs the details for a collection. + /// Writes messages to the console and log file. + /// The collection. + /// The interval over which to calculate the averages. + /// The threshold. + /// The source filter. + private void OutputPerformanceCollectionDetail(IMonitor monitor, PerformanceCounterCollection collection, + TimeSpan averageInterval, double? thresholdMilliseconds, string sourceFilter = null) + { + StringBuilder sb = new StringBuilder($"Performance Counter for {collection.Name}:\n\n"); + + List> data = collection.PerformanceCounters.ToList(); + + if (sourceFilter != null) + { + data = collection.PerformanceCounters.Where(p => + p.Value.Source.ToLowerInvariant().Contains(sourceFilter.ToLowerInvariant())).ToList(); + } + + if (thresholdMilliseconds != null) + { + data = data.Where(p => p.Value.GetAverage(averageInterval) >= thresholdMilliseconds).ToList(); + } + + if (data.Any()) + { + sb.AppendLine(this.GetTableString( + data: data, + header: new[] {"Mod", $"Avg Execution Time (last {(int) averageInterval.TotalSeconds}s)", "Last Execution Time", "Peak Execution Time"}, + getRow: item => new[] + { + item.Key, + this.FormatMilliseconds(item.Value.GetAverage(averageInterval), thresholdMilliseconds), + this.FormatMilliseconds(item.Value.GetLastEntry()?.ElapsedMilliseconds), + this.FormatMilliseconds(item.Value.GetPeak()?.ElapsedMilliseconds) + }, + true + )); + } + else + { + sb.Clear(); + sb.AppendLine($"Performance Counter for {collection.Name}: none."); + } + + monitor.Log(sb.ToString(), LogLevel.Info); + } + + /// Parses a command string and returns the associated command. + /// The command string + /// The parsed command. + private SubCommand ParseCommandString(string commandString) + { + foreach (var i in this.SubCommandNames.Where(i => + i.Value.Any(str => str.Equals(commandString, StringComparison.InvariantCultureIgnoreCase)))) + { + return i.Key; + } + + return SubCommand.None; + } + + + /// Formats the given milliseconds value into a string format. Optionally + /// allows a threshold to return "-" if the value is less than the threshold. + /// The milliseconds to format. Returns "-" if null + /// The threshold. Any value below this is returned as "-". + /// The formatted milliseconds. + private string FormatMilliseconds(double? milliseconds, double? thresholdMilliseconds = null) + { + if (milliseconds == null || (thresholdMilliseconds != null && milliseconds < thresholdMilliseconds)) + { + return "-"; + } + + return ((double) milliseconds).ToString("F2"); + } + + /// Shows detailed help for a specific sub command. + /// The output monitor + /// The sub command + private void OutputHelp(IMonitor monitor, SubCommand subCommand) { StringBuilder sb = new StringBuilder(); sb.AppendLine(); - switch (command) + + switch (subCommand) { - case Command.Concepts: + case SubCommand.Concepts: sb.AppendLine("A performance counter is a metric which measures execution time. Each performance"); sb.AppendLine("counter consists of:"); sb.AppendLine(); @@ -271,7 +511,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other sb.AppendLine(); sb.AppendLine("[1] https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Events"); break; - case Command.Detail: + case SubCommand.Detail: sb.AppendLine("Usage: pc detail "); sb.AppendLine(" pc detail "); sb.AppendLine(); @@ -288,61 +528,66 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other sb.AppendLine("pc detail Display.Rendering Pathoschild.ChestsAnywhere Displays the 'Display.Rendering' performance counter for 'Pathoschild.ChestsAnywhere'"); sb.AppendLine("pc detail Display.Rendering 5 Displays the 'Display.Rendering' performance counters exceeding an average of 5ms"); break; - case Command.Summary: - sb.AppendLine("Usage: pc summary "); + case SubCommand.Summary: + sb.AppendLine("Usage: pc summary "); sb.AppendLine(); sb.AppendLine("Displays the performance counter summary."); sb.AppendLine(); sb.AppendLine("Arguments:"); - sb.AppendLine(" Optional. Defaults to 'important' if omitted. Specifies one of these modes:"); - sb.AppendLine(" - all Displays performance counters from all collections"); - sb.AppendLine(" - important Displays only important performance counter collections"); + sb.AppendLine(" Optional. Defaults to 'important' if omitted. Specifies one of these modes:"); + sb.AppendLine(" - all Displays performance counters from all collections"); + sb.AppendLine(" - important Displays only important performance counter collections"); sb.AppendLine(); - sb.AppendLine(" Optional. Only shows performance counter collections matching the given name"); + sb.AppendLine(" Optional. Only shows performance counter collections matching the given name"); + sb.AppendLine(" Optional. Hides the actual execution time if it is below this threshold"); sb.AppendLine(); sb.AppendLine("Examples:"); sb.AppendLine("pc summary all Shows all events"); + sb.AppendLine("pc summary all 5 Shows all events"); sb.AppendLine("pc summary Display.Rendering Shows only the 'Display.Rendering' collection"); break; - case Command.Monitor: - sb.AppendLine("Usage: pc monitor "); - sb.AppendLine("Usage: pc monitor "); + case SubCommand.Trigger: + sb.AppendLine("Usage: pc trigger "); + sb.AppendLine("Usage: pc trigger collection "); + sb.AppendLine("Usage: pc trigger collection "); sb.AppendLine(); - sb.AppendLine("Manages monitoring settings."); + sb.AppendLine("Manages alert triggers."); sb.AppendLine(); sb.AppendLine("Arguments:"); - sb.AppendLine(" Optional. Specifies if a specific source or a specific collection should be monitored."); - sb.AppendLine(" - list Lists current monitoring settings"); - sb.AppendLine(" - collection Sets up a monitor for a collection"); - sb.AppendLine(" - clear Clears all monitoring entries"); + sb.AppendLine(" Optional. Specifies if a specific source or a specific collection should be triggered."); + sb.AppendLine(" - list Lists current triggers"); + sb.AppendLine(" - collection Sets up a trigger for a collection"); + sb.AppendLine(" - clear Clears all trigger entries"); + sb.AppendLine(" - pause Pauses triggering of alerts"); + sb.AppendLine(" - resume Resumes triggering of alerts"); sb.AppendLine(" Defaults to 'list' if not specified."); sb.AppendLine(); sb.AppendLine(" Required if the mode 'collection' is specified."); - sb.AppendLine(" Specifies the name of the collection to be monitored. Must be an exact match."); + sb.AppendLine(" Specifies the name of the collection to be triggered. Must be an exact match."); sb.AppendLine(); sb.AppendLine(" Optional. Specifies the name of a specific source. Must be an exact match."); sb.AppendLine(); sb.AppendLine(" Required if the mode 'collection' is specified."); sb.AppendLine(" Specifies the threshold in milliseconds (fractions allowed)."); - sb.AppendLine(" Can also be 'remove' to remove the threshold."); + sb.AppendLine(" Specify '0' to remove the threshold."); sb.AppendLine(); sb.AppendLine("Examples:"); sb.AppendLine(); - sb.AppendLine("pc monitor collection Display.Rendering 10"); - sb.AppendLine(" Sets up monitoring to write an alert on the console if the execution time of all performance counters in"); + sb.AppendLine("pc trigger collection Display.Rendering 10"); + sb.AppendLine(" Sets up an alert trigger which writes on the console if the execution time of all performance counters in"); sb.AppendLine(" the 'Display.Rendering' collection exceed 10 milliseconds."); sb.AppendLine(); - sb.AppendLine("pc monitor collection Display.Rendering Pathoschild.ChestsAnywhere 5"); - sb.AppendLine(" Sets up monitoring to write an alert on the console if the execution time of Pathoschild.ChestsAnywhere in"); + sb.AppendLine("pc trigger collection Display.Rendering 5 Pathoschild.ChestsAnywhere"); + sb.AppendLine(" Sets up an alert trigger to write on the console if the execution time of Pathoschild.ChestsAnywhere in"); sb.AppendLine(" the 'Display.Rendering' collection exceed 5 milliseconds."); sb.AppendLine(); - sb.AppendLine("pc monitor collection Display.Rendering remove"); + sb.AppendLine("pc trigger collection Display.Rendering 0"); sb.AppendLine(" Removes the threshold previously defined from the collection. Note that source-specific thresholds are left intact."); sb.AppendLine(); - sb.AppendLine("pc monitor clear"); - sb.AppendLine(" Clears all previously setup monitors."); + sb.AppendLine("pc trigger clear"); + sb.AppendLine(" Clears all previously setup alert triggers."); break; - case Command.Reset: + case SubCommand.Reset: sb.AppendLine("Usage: pc reset "); sb.AppendLine(); sb.AppendLine("Resets performance counters."); @@ -368,153 +613,12 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other monitor.Log(sb.ToString(), LogLevel.Info); } - private void ResetCounter(IMonitor monitor, ArgumentParser args) - { - if (args.TryGet(1, "type", out string type, false)) - { - args.TryGet(2, "name", out string name); - - switch (type) - { - case "category": - SCore.PerformanceCounterManager.ResetCollection(name); - monitor.Log($"All performance counters for category {name} are now cleared.", LogLevel.Info); - break; - case "mod": - SCore.PerformanceCounterManager.ResetSource(name); - monitor.Log($"All performance counters for mod {name} are now cleared.", LogLevel.Info); - break; - } - } - else - { - SCore.PerformanceCounterManager.Reset(); - monitor.Log("All performance counters are now cleared.", LogLevel.Info); - } - } - - private void DisplayPerformanceCounterSummary(IMonitor monitor, ArgumentParser args) - { - IEnumerable data; - - if (!args.TryGet(1, "mode", out string mode, false)) - { - mode = "important"; - } - - switch (mode) - { - case null: - case "important": - data = SCore.PerformanceCounterManager.PerformanceCounterCollections.Where(p => p.IsImportant); - break; - case "all": - data = SCore.PerformanceCounterManager.PerformanceCounterCollections; - break; - default: - data = SCore.PerformanceCounterManager.PerformanceCounterCollections.Where(p => - p.Name.ToLowerInvariant().Contains(mode.ToLowerInvariant())); - break; - } - - StringBuilder sb = new StringBuilder(); - - sb.AppendLine("Summary:"); - sb.AppendLine(this.GetTableString( - data: data, - header: new[] {"Collection", "Avg Calls/s", "Avg Execution Time (Game)", "Avg Execution Time (Mods)", "Avg Execution Time (Game+Mods)"}, - getRow: item => new[] - { - item.Name, - item.GetAverageCallsPerSecond().ToString(), - this.FormatMilliseconds(item.GetGameAverageExecutionTime()), - this.FormatMilliseconds(item.GetModsAverageExecutionTime()), - this.FormatMilliseconds(item.GetAverageExecutionTime()) - } - )); - - monitor.Log(sb.ToString(), LogLevel.Info); - } - - private void DisplayPerformanceCounterDetail(IMonitor monitor, ArgumentParser args) - { - List collections = new List(); - TimeSpan averageInterval = TimeSpan.FromSeconds(60); - double? thresholdMilliseconds = null; - string sourceFilter = null; - - if (args.TryGet(1, "collection", out string collectionName)) - { - collections.AddRange(SCore.PerformanceCounterManager.PerformanceCounterCollections.Where(collection => collection.Name.ToLowerInvariant().Contains(collectionName.ToLowerInvariant()))); - - if (args.IsDecimal(2) && args.TryGetDecimal(2, "threshold", out decimal value, false)) - { - thresholdMilliseconds = (double?) value; - } - else - { - if (args.TryGet(2, "source", out string sourceName, false)) - { - sourceFilter = sourceName; - } - } - } - - foreach (var c in collections) - { - this.DisplayPerformanceCollectionDetail(monitor, c, averageInterval, thresholdMilliseconds, sourceFilter); - } - } - - private void DisplayPerformanceCollectionDetail(IMonitor monitor, PerformanceCounterCollection collection, - TimeSpan averageInterval, double? thresholdMilliseconds, string sourceFilter = null) - { - StringBuilder sb = new StringBuilder($"Performance Counter for {collection.Name}:\n\n"); - - IEnumerable> data = collection.PerformanceCounters; - - if (sourceFilter != null) - { - data = collection.PerformanceCounters.Where(p => - p.Value.Source.ToLowerInvariant().Contains(sourceFilter.ToLowerInvariant())); - } - - if (thresholdMilliseconds != null) - { - data = data.Where(p => p.Value.GetAverage(averageInterval) >= thresholdMilliseconds); - } - - sb.AppendLine(this.GetTableString( - data: data, - header: new[] {"Mod", $"Avg Execution Time (last {(int) averageInterval.TotalSeconds}s)", "Last Execution Time", "Peak Execution Time"}, - getRow: item => new[] - { - item.Key, - this.FormatMilliseconds(item.Value.GetAverage(averageInterval), thresholdMilliseconds), - this.FormatMilliseconds(item.Value.GetLastEntry()?.ElapsedMilliseconds), - this.FormatMilliseconds(item.Value.GetPeak()?.ElapsedMilliseconds) - } - )); - - monitor.Log(sb.ToString(), LogLevel.Info); - } - - private string FormatMilliseconds(double? milliseconds, double? thresholdMilliseconds = null) - { - if (milliseconds == null || (thresholdMilliseconds != null && milliseconds < thresholdMilliseconds)) - { - return "-"; - } - - return ((double) milliseconds).ToString("F2"); - } - /// Get the command description. private static string GetDescription() { StringBuilder sb = new StringBuilder(); - sb.AppendLine("Displays and configured performance counters."); + sb.AppendLine("Displays and configures performance counters."); sb.AppendLine(); sb.AppendLine("A performance counter records the invocation time of in-game events being"); sb.AppendLine("processed by mods or the game itself. See 'concepts' for a detailed explanation."); @@ -526,7 +630,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other sb.AppendLine(" summary|sum|s Displays a summary of important or all collections"); sb.AppendLine(" detail|d Shows performance counter information for a given collection"); sb.AppendLine(" reset|r Resets the performance counters"); - sb.AppendLine(" monitor Configures monitoring settings"); + sb.AppendLine(" trigger Configures alert triggers"); sb.AppendLine(" examples Displays various examples"); sb.AppendLine(" concepts Displays an explanation of the performance counter concepts"); sb.AppendLine(" help Displays verbose help for the available commands"); diff --git a/src/SMAPI/Framework/Events/EventManager.cs b/src/SMAPI/Framework/Events/EventManager.cs index 9c65a6cc..19a4dff8 100644 --- a/src/SMAPI/Framework/Events/EventManager.cs +++ b/src/SMAPI/Framework/Events/EventManager.cs @@ -174,6 +174,7 @@ namespace StardewModdingAPI.Framework.Events /// Construct an instance. /// Writes messages to the log. /// The mod registry with which to identify mods. + /// The performance counter manager. public EventManager(IMonitor monitor, ModRegistry modRegistry, PerformanceCounterManager performanceCounterManager) { // create shortcut initializers diff --git a/src/SMAPI/Framework/Events/ManagedEvent.cs b/src/SMAPI/Framework/Events/ManagedEvent.cs index bba94c35..dfdd7449 100644 --- a/src/SMAPI/Framework/Events/ManagedEvent.cs +++ b/src/SMAPI/Framework/Events/ManagedEvent.cs @@ -40,6 +40,7 @@ namespace StardewModdingAPI.Framework.Events /// A human-readable name for the event. /// Writes messages to the log. /// The mod registry with which to identify mods. + /// The performance counter manager public ManagedEvent(string eventName, IMonitor monitor, ModRegistry modRegistry, PerformanceCounterManager performanceCounterManager) { this.EventName = eventName; diff --git a/src/SMAPI/Framework/PerformanceCounter/PerformanceCounter.cs b/src/SMAPI/Framework/PerformanceCounter/PerformanceCounter.cs index b2ec4c90..33ddde2f 100644 --- a/src/SMAPI/Framework/PerformanceCounter/PerformanceCounter.cs +++ b/src/SMAPI/Framework/PerformanceCounter/PerformanceCounter.cs @@ -114,6 +114,10 @@ namespace StardewModdingAPI.Framework.PerformanceCounter DateTime start = relativeTo.Value.Subtract(range); var entries = this._counter.Where(x => (x.EventTime >= start) && (x.EventTime <= relativeTo)); + + if (!entries.Any()) + return 0; + return entries.Average(x => x.ElapsedMilliseconds); } } diff --git a/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterManager.cs b/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterManager.cs index d8f1f172..49720431 100644 --- a/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterManager.cs +++ b/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterManager.cs @@ -20,6 +20,9 @@ namespace StardewModdingAPI.Framework.PerformanceCounter /// The invocation stopwatch. private readonly Stopwatch InvocationStopwatch = new Stopwatch(); + /// Specifies if alerts should be paused. + public bool PauseAlerts { get; set; } + /// Constructs a performance counter manager. /// The monitor for output logging. public PerformanceCounterManager(IMonitor monitor) @@ -144,7 +147,8 @@ namespace StardewModdingAPI.Framework.PerformanceCounter /// The alert to add. public void AddAlert(AlertEntry entry) { - this.Alerts.Add(entry); + if (!this.PauseAlerts) + this.Alerts.Add(entry); } /// Initialized the default performance counter collections. -- cgit From 238b5db4f7f2f05e8967cc5eda761733d4bf35b4 Mon Sep 17 00:00:00 2001 From: Drachenkaetzchen Date: Wed, 15 Jan 2020 17:50:12 +0100 Subject: Added "trigger dump" command to dump the configured triggers as commands for copy'n'paste --- .../Commands/Other/PerformanceCounterCommand.cs | 67 +++++++++++++++------- 1 file changed, 47 insertions(+), 20 deletions(-) (limited to 'src/SMAPI.Mods.ConsoleCommands/Framework') diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/PerformanceCounterCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/PerformanceCounterCommand.cs index 82b44562..f096614f 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/PerformanceCounterCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/PerformanceCounterCommand.cs @@ -206,6 +206,9 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other SCore.PerformanceCounterManager.PauseAlerts = false; monitor.Log($"Alerts are now resumed.", LogLevel.Info); break; + case "dump": + this.OutputAlertTriggers(monitor, true); + break; case "clear": this.ClearAlertTriggers(monitor); break; @@ -308,7 +311,8 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other /// Lists all configured alert triggers. /// Writes messages to the console and log file. - private void OutputAlertTriggers(IMonitor monitor) + /// True to dump the triggers as commands. + private void OutputAlertTriggers(IMonitor monitor, bool asDump = false) { StringBuilder sb = new StringBuilder(); sb.AppendLine("Configured triggers:"); @@ -332,16 +336,27 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other { sb.AppendLine("Collection Triggers:"); sb.AppendLine(); - sb.AppendLine(this.GetTableString( - data: collectionTriggers, - header: new[] {"Collection", "Threshold"}, - getRow: item => new[] + + if (asDump) + { + foreach (var item in collectionTriggers) { - item.collectionName, - this.FormatMilliseconds(item.threshold) - }, - true - )); + sb.AppendLine($"pc trigger {item.collectionName} {item.threshold}"); + } + } + else + { + sb.AppendLine(this.GetTableString( + data: collectionTriggers, + header: new[] {"Collection", "Threshold"}, + getRow: item => new[] + { + item.collectionName, + this.FormatMilliseconds(item.threshold) + }, + true + )); + } sb.AppendLine(); } @@ -354,17 +369,28 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other { sb.AppendLine("Source Triggers:"); sb.AppendLine(); - sb.AppendLine(this.GetTableString( - data: sourceTriggers, - header: new[] {"Collection", "Source", "Threshold"}, - getRow: item => new[] + + if (asDump) + { + foreach (var item in sourceTriggers) { - item.collectionName, - item.sourceName, - this.FormatMilliseconds(item.threshold) - }, - true - )); + sb.AppendLine($"pc trigger {item.collectionName} {item.threshold} {item.sourceName}"); + } + } + else + { + sb.AppendLine(this.GetTableString( + data: sourceTriggers, + header: new[] {"Collection", "Source", "Threshold"}, + getRow: item => new[] + { + item.collectionName, + item.sourceName, + this.FormatMilliseconds(item.threshold) + }, + true + )); + } sb.AppendLine(); } @@ -560,6 +586,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other sb.AppendLine(" - clear Clears all trigger entries"); sb.AppendLine(" - pause Pauses triggering of alerts"); sb.AppendLine(" - resume Resumes triggering of alerts"); + sb.AppendLine(" - dump Dumps all triggers as commands for copy and paste"); sb.AppendLine(" Defaults to 'list' if not specified."); sb.AppendLine(); sb.AppendLine(" Required if the mode 'collection' is specified."); -- cgit From 84973ce5727ad20fe8b8ba4f89e59c8b754f799e Mon Sep 17 00:00:00 2001 From: Drachenkaetzchen Date: Wed, 15 Jan 2020 19:08:50 +0100 Subject: Added peak execution time over the last 60 seconds --- .../Commands/Other/PerformanceCounterCommand.cs | 12 ++++-- .../Framework/PerformanceCounter/PeakEntry.cs | 24 +++++++++++ .../PerformanceCounter/PerformanceCounter.cs | 20 +++++++++ .../PerformanceCounterCollection.cs | 47 ++++++++++++++++++---- .../PerformanceCounterManager.cs | 5 +++ 5 files changed, 96 insertions(+), 12 deletions(-) create mode 100644 src/SMAPI/Framework/PerformanceCounter/PeakEntry.cs (limited to 'src/SMAPI.Mods.ConsoleCommands/Framework') diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/PerformanceCounterCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/PerformanceCounterCommand.cs index f096614f..d49fc537 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/PerformanceCounterCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/PerformanceCounterCommand.cs @@ -118,17 +118,20 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other StringBuilder sb = new StringBuilder(); + TimeSpan peakSpan = TimeSpan.FromSeconds(60); + sb.AppendLine("Summary:"); sb.AppendLine(this.GetTableString( data: data, - header: new[] {"Collection", "Avg Calls/s", "Avg Execution Time (Game)", "Avg Execution Time (Mods)", "Avg Execution Time (Game+Mods)"}, + header: new[] {"Collection", "Avg Calls/s", "Avg Exec Time (Game)", "Avg Exec Time (Mods)", "Avg Exec Time (Game+Mods)", "Peak Exec Time (60s)"}, getRow: item => new[] { item.Name, item.GetAverageCallsPerSecond().ToString(), this.FormatMilliseconds(item.GetGameAverageExecutionTime(), threshold), this.FormatMilliseconds(item.GetModsAverageExecutionTime(), threshold), - this.FormatMilliseconds(item.GetAverageExecutionTime(), threshold) + this.FormatMilliseconds(item.GetAverageExecutionTime(), threshold), + this.FormatMilliseconds(item.GetPeakExecutionTime(peakSpan), threshold) }, true )); @@ -459,13 +462,14 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other { sb.AppendLine(this.GetTableString( data: data, - header: new[] {"Mod", $"Avg Execution Time (last {(int) averageInterval.TotalSeconds}s)", "Last Execution Time", "Peak Execution Time"}, + header: new[] {"Mod", $"Avg Exec Time (last {(int) averageInterval.TotalSeconds}s)", "Last Exec Time", "Peak Exec Time", $"Peak Exec Time (last {(int) averageInterval.TotalSeconds}s)"}, getRow: item => new[] { item.Key, this.FormatMilliseconds(item.Value.GetAverage(averageInterval), thresholdMilliseconds), this.FormatMilliseconds(item.Value.GetLastEntry()?.ElapsedMilliseconds), - this.FormatMilliseconds(item.Value.GetPeak()?.ElapsedMilliseconds) + this.FormatMilliseconds(item.Value.GetPeak()?.ElapsedMilliseconds), + this.FormatMilliseconds(item.Value.GetPeak(averageInterval)?.ElapsedMilliseconds) }, true )); diff --git a/src/SMAPI/Framework/PerformanceCounter/PeakEntry.cs b/src/SMAPI/Framework/PerformanceCounter/PeakEntry.cs new file mode 100644 index 00000000..95dc11f4 --- /dev/null +++ b/src/SMAPI/Framework/PerformanceCounter/PeakEntry.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; + +namespace StardewModdingAPI.Framework.PerformanceCounter +{ + internal struct PeakEntry + { + /// The actual execution time in milliseconds. + public readonly double ExecutionTimeMilliseconds; + + /// The DateTime when the entry occured. + public DateTime EventTime; + + /// The context list, which records all sources involved in exceeding the threshold. + public readonly List Context; + + public PeakEntry(double executionTimeMilliseconds, DateTime eventTime, List context) + { + this.ExecutionTimeMilliseconds = executionTimeMilliseconds; + this.EventTime = eventTime; + this.Context = context; + } + } +} diff --git a/src/SMAPI/Framework/PerformanceCounter/PerformanceCounter.cs b/src/SMAPI/Framework/PerformanceCounter/PerformanceCounter.cs index 33ddde2f..3d902e16 100644 --- a/src/SMAPI/Framework/PerformanceCounter/PerformanceCounter.cs +++ b/src/SMAPI/Framework/PerformanceCounter/PerformanceCounter.cs @@ -68,6 +68,26 @@ namespace StardewModdingAPI.Framework.PerformanceCounter return this.PeakPerformanceCounterEntry; } + /// Returns the peak entry. + /// The peak entry. + public PerformanceCounterEntry? GetPeak(TimeSpan range, DateTime? relativeTo = null) + { + if (this._counter.IsEmpty) + return null; + + if (relativeTo == null) + relativeTo = DateTime.UtcNow; + + DateTime start = relativeTo.Value.Subtract(range); + + var entries = this._counter.Where(x => (x.EventTime >= start) && (x.EventTime <= relativeTo)).ToList(); + + if (!entries.Any()) + return null; + + return entries.OrderByDescending(x => x.ElapsedMilliseconds).First(); + } + /// Resets the peak entry. public void ResetPeak() { diff --git a/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterCollection.cs b/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterCollection.cs index b48efd67..fe14ebf8 100644 --- a/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterCollection.cs +++ b/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterCollection.cs @@ -2,11 +2,15 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using Cyotek.Collections.Generic; namespace StardewModdingAPI.Framework.PerformanceCounter { internal class PerformanceCounterCollection { + /// The size of the ring buffer. + private const int MAX_ENTRIES = 16384; + /// The list of triggered performance counters. private readonly List TriggeredPerformanceCounters = new List(); @@ -22,6 +26,10 @@ namespace StardewModdingAPI.Framework.PerformanceCounter /// The number of invocations of this collection. private long CallCount; + /// The circular buffer which stores all peak invocations + private readonly CircularBuffer PeakInvocations; + + /// The associated performance counters. public IDictionary PerformanceCounters { get; } = new Dictionary(); /// The name of this collection. @@ -39,6 +47,7 @@ namespace StardewModdingAPI.Framework.PerformanceCounter public PerformanceCounterCollection(PerformanceCounterManager performanceCounterManager, string name, bool isImportant) { + this.PeakInvocations = new CircularBuffer(PerformanceCounterCollection.MAX_ENTRIES); this.Name = name; this.PerformanceCounterManager = performanceCounterManager; this.IsImportant = isImportant; @@ -46,6 +55,7 @@ namespace StardewModdingAPI.Framework.PerformanceCounter public PerformanceCounterCollection(PerformanceCounterManager performanceCounterManager, string name) { + this.PeakInvocations = new CircularBuffer(PerformanceCounterCollection.MAX_ENTRIES); this.PerformanceCounterManager = performanceCounterManager; this.Name = name; } @@ -89,15 +99,30 @@ namespace StardewModdingAPI.Framework.PerformanceCounter return 0; } + public double GetPeakExecutionTime(TimeSpan range, DateTime? relativeTo = null) + { + if (this.PeakInvocations.IsEmpty) + return 0; + + if (relativeTo == null) + relativeTo = DateTime.UtcNow; + + DateTime start = relativeTo.Value.Subtract(range); + + var entries = this.PeakInvocations.Where(x => (x.EventTime >= start) && (x.EventTime <= relativeTo)).ToList(); + + if (!entries.Any()) + return 0; + + return entries.OrderByDescending(x => x.ExecutionTimeMilliseconds).First().ExecutionTimeMilliseconds; + } + /// Begins tracking the invocation of this collection. public void BeginTrackInvocation() { - if (this.EnableAlerts) - { - this.TriggeredPerformanceCounters.Clear(); - this.InvocationStopwatch.Reset(); - this.InvocationStopwatch.Start(); - } + this.TriggeredPerformanceCounters.Clear(); + this.InvocationStopwatch.Reset(); + this.InvocationStopwatch.Start(); this.CallCount++; } @@ -106,10 +131,15 @@ namespace StardewModdingAPI.Framework.PerformanceCounter /// and the invocation time exceeds the threshold. public void EndTrackInvocation() { - if (!this.EnableAlerts) return; - this.InvocationStopwatch.Stop(); + this.PeakInvocations.Put( + new PeakEntry(this.InvocationStopwatch.Elapsed.TotalMilliseconds, + DateTime.UtcNow, + this.TriggeredPerformanceCounters)); + + if (!this.EnableAlerts) return; + if (this.InvocationStopwatch.Elapsed.TotalMilliseconds >= this.AlertThresholdMilliseconds) this.AddAlert(this.InvocationStopwatch.Elapsed.TotalMilliseconds, this.AlertThresholdMilliseconds, this.TriggeredPerformanceCounters); @@ -144,6 +174,7 @@ namespace StardewModdingAPI.Framework.PerformanceCounter /// Resets all performance counters in this collection. public void Reset() { + this.PeakInvocations.Clear(); foreach (var i in this.PerformanceCounters) i.Value.Reset(); } diff --git a/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterManager.cs b/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterManager.cs index 49720431..a8e20eda 100644 --- a/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterManager.cs +++ b/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterManager.cs @@ -33,6 +33,11 @@ namespace StardewModdingAPI.Framework.PerformanceCounter /// Resets all performance counters in all collections. public void Reset() { + foreach (PerformanceCounterCollection collection in this.PerformanceCounterCollections) + { + collection.Reset(); + } + foreach (var eventPerformanceCounter in this.PerformanceCounterCollections.SelectMany(performanceCounter => performanceCounter.PerformanceCounters)) { -- cgit From 1b905205a3073c56e29c46b5e57c4a9cb2ca5832 Mon Sep 17 00:00:00 2001 From: Drachenkaetzchen Date: Tue, 21 Jan 2020 12:20:06 +0100 Subject: Added commands to enable and disable performance counters. Peak is now using the default interval --- .../Commands/Other/PerformanceCounterCommand.cs | 28 ++++++++++++++----- .../PerformanceCounter/PerformanceCounter.cs | 2 +- .../PerformanceCounterCollection.cs | 31 ++++++++++++++++++++++ .../PerformanceCounterManager.cs | 19 +++++++++++++ src/SMAPI/SMAPI.csproj | 4 +++ 5 files changed, 76 insertions(+), 8 deletions(-) (limited to 'src/SMAPI.Mods.ConsoleCommands/Framework') diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/PerformanceCounterCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/PerformanceCounterCommand.cs index d49fc537..2260296b 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/PerformanceCounterCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/PerformanceCounterCommand.cs @@ -17,6 +17,8 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other {SubCommand.Detail, new[] {"detail", "d"}}, {SubCommand.Reset, new[] {"reset", "r"}}, {SubCommand.Trigger, new[] {"trigger"}}, + {SubCommand.Enable, new[] {"enable"}}, + {SubCommand.Disable, new[] {"disable"}}, {SubCommand.Examples, new[] {"examples"}}, {SubCommand.Concepts, new[] {"concepts"}}, {SubCommand.Help, new[] {"help"}}, @@ -29,6 +31,8 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other Detail, Reset, Trigger, + Enable, + Disable, Examples, Help, Concepts, @@ -69,6 +73,14 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other case SubCommand.Concepts: this.OutputHelp(monitor, SubCommand.Concepts); break; + case SubCommand.Enable: + SCore.PerformanceCounterManager.EnableTracking = true; + monitor.Log("Performance counter tracking is now enabled", LogLevel.Info); + break; + case SubCommand.Disable: + SCore.PerformanceCounterManager.EnableTracking = false; + monitor.Log("Performance counter tracking is now disabled", LogLevel.Info); + break; case SubCommand.Help: if (args.TryGet(1, "command", out string commandString)) this.OutputHelp(monitor, this.ParseCommandString(commandString)); @@ -118,20 +130,20 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other StringBuilder sb = new StringBuilder(); - TimeSpan peakSpan = TimeSpan.FromSeconds(60); + TimeSpan interval = TimeSpan.FromSeconds(60); - sb.AppendLine("Summary:"); + sb.AppendLine($"Summary over the last {interval.TotalSeconds} seconds:"); sb.AppendLine(this.GetTableString( data: data, - header: new[] {"Collection", "Avg Calls/s", "Avg Exec Time (Game)", "Avg Exec Time (Mods)", "Avg Exec Time (Game+Mods)", "Peak Exec Time (60s)"}, + header: new[] {"Collection", "Avg Calls/s", "Avg Exec Time (Game)", "Avg Exec Time (Mods)", "Avg Exec Time (Game+Mods)", "Peak Exec Time"}, getRow: item => new[] { item.Name, item.GetAverageCallsPerSecond().ToString(), - this.FormatMilliseconds(item.GetGameAverageExecutionTime(), threshold), - this.FormatMilliseconds(item.GetModsAverageExecutionTime(), threshold), - this.FormatMilliseconds(item.GetAverageExecutionTime(), threshold), - this.FormatMilliseconds(item.GetPeakExecutionTime(peakSpan), threshold) + this.FormatMilliseconds(item.GetGameAverageExecutionTime(interval), threshold), + this.FormatMilliseconds(item.GetModsAverageExecutionTime(interval), threshold), + this.FormatMilliseconds(item.GetAverageExecutionTime(interval), threshold), + this.FormatMilliseconds(item.GetPeakExecutionTime(interval), threshold) }, true )); @@ -662,6 +674,8 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other sb.AppendLine(" detail|d Shows performance counter information for a given collection"); sb.AppendLine(" reset|r Resets the performance counters"); sb.AppendLine(" trigger Configures alert triggers"); + sb.AppendLine(" enable Enables performance counter recording"); + sb.AppendLine(" disable Disables performance counter recording"); sb.AppendLine(" examples Displays various examples"); sb.AppendLine(" concepts Displays an explanation of the performance counter concepts"); sb.AppendLine(" help Displays verbose help for the available commands"); diff --git a/src/SMAPI/Framework/PerformanceCounter/PerformanceCounter.cs b/src/SMAPI/Framework/PerformanceCounter/PerformanceCounter.cs index 3d902e16..e9dfcb14 100644 --- a/src/SMAPI/Framework/PerformanceCounter/PerformanceCounter.cs +++ b/src/SMAPI/Framework/PerformanceCounter/PerformanceCounter.cs @@ -133,7 +133,7 @@ namespace StardewModdingAPI.Framework.PerformanceCounter DateTime start = relativeTo.Value.Subtract(range); - var entries = this._counter.Where(x => (x.EventTime >= start) && (x.EventTime <= relativeTo)); + var entries = this._counter.Where(x => (x.EventTime >= start) && (x.EventTime <= relativeTo)).ToList(); if (!entries.Any()) return 0; diff --git a/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterCollection.cs b/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterCollection.cs index fe14ebf8..f469eceb 100644 --- a/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterCollection.cs +++ b/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterCollection.cs @@ -82,6 +82,15 @@ namespace StardewModdingAPI.Framework.PerformanceCounter p.Key != Constants.GamePerformanceCounterName).Sum(p => p.Value.GetAverage()); } + /// Returns the average execution time for all non-game internal sources. + /// The interval for which to get the average, relative to now + /// The average execution time in milliseconds + public double GetModsAverageExecutionTime(TimeSpan interval) + { + return this.PerformanceCounters.Where(p => + p.Key != Constants.GamePerformanceCounterName).Sum(p => p.Value.GetAverage(interval)); + } + /// Returns the overall average execution time. /// The average execution time in milliseconds public double GetAverageExecutionTime() @@ -89,6 +98,14 @@ namespace StardewModdingAPI.Framework.PerformanceCounter return this.PerformanceCounters.Sum(p => p.Value.GetAverage()); } + /// Returns the overall average execution time. + /// The interval for which to get the average, relative to now + /// The average execution time in milliseconds + public double GetAverageExecutionTime(TimeSpan interval) + { + return this.PerformanceCounters.Sum(p => p.Value.GetAverage(interval)); + } + /// Returns the average execution time for game-internal sources. /// The average execution time in milliseconds public double GetGameAverageExecutionTime() @@ -99,6 +116,20 @@ namespace StardewModdingAPI.Framework.PerformanceCounter return 0; } + /// Returns the average execution time for game-internal sources. + /// The average execution time in milliseconds + public double GetGameAverageExecutionTime(TimeSpan interval) + { + if (this.PerformanceCounters.TryGetValue(Constants.GamePerformanceCounterName, out PerformanceCounter gameExecTime)) + return gameExecTime.GetAverage(interval); + + return 0; + } + + /// Returns the peak execution time + /// The interval for which to get the peak, relative to + /// The DateTime which the is relative to, or DateTime.Now if not given + /// The peak execution time public double GetPeakExecutionTime(TimeSpan range, DateTime? relativeTo = null) { if (this.PeakInvocations.IsEmpty) diff --git a/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterManager.cs b/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterManager.cs index a8e20eda..bd964442 100644 --- a/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterManager.cs +++ b/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterManager.cs @@ -23,6 +23,9 @@ namespace StardewModdingAPI.Framework.PerformanceCounter /// Specifies if alerts should be paused. public bool PauseAlerts { get; set; } + /// Specifies if performance counter tracking should be enabled. + public bool EnableTracking { get; set; } + /// Constructs a performance counter manager. /// The monitor for output logging. public PerformanceCounterManager(IMonitor monitor) @@ -49,6 +52,11 @@ namespace StardewModdingAPI.Framework.PerformanceCounter /// The collection name public void BeginTrackInvocation(string collectionName) { + if (!this.EnableTracking) + { + return; + } + this.GetOrCreateCollectionByName(collectionName).BeginTrackInvocation(); } @@ -56,6 +64,11 @@ namespace StardewModdingAPI.Framework.PerformanceCounter /// public void EndTrackInvocation(string collectionName) { + if (!this.EnableTracking) + { + return; + } + this.GetOrCreateCollectionByName(collectionName).EndTrackInvocation(); } @@ -65,6 +78,12 @@ namespace StardewModdingAPI.Framework.PerformanceCounter /// The action to execute and track invocation time for. public void Track(string collectionName, string sourceName, Action action) { + if (!this.EnableTracking) + { + action(); + return; + } + DateTime eventTime = DateTime.UtcNow; this.InvocationStopwatch.Reset(); this.InvocationStopwatch.Start(); diff --git a/src/SMAPI/SMAPI.csproj b/src/SMAPI/SMAPI.csproj index 5e407c2c..0bc290ac 100644 --- a/src/SMAPI/SMAPI.csproj +++ b/src/SMAPI/SMAPI.csproj @@ -15,6 +15,10 @@ icon.ico + + SMAPI_FOR_WINDOWS + + -- cgit From 381de5eba9f9822c3483abdf64396cec794e3d03 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 22 Jan 2020 20:36:24 -0500 Subject: add test_input console command --- docs/release-notes.md | 14 +++-- .../Framework/Commands/ITrainerCommand.cs | 14 +++-- .../Framework/Commands/Other/TestInputCommand.cs | 59 ++++++++++++++++++++++ .../Framework/Commands/Player/SetHealthCommand.cs | 15 ++---- .../Framework/Commands/Player/SetMoneyCommand.cs | 13 ++--- .../Framework/Commands/Player/SetStaminaCommand.cs | 15 ++---- .../Framework/Commands/TrainerCommand.cs | 22 ++++++-- .../Framework/Commands/World/FreezeTimeCommand.cs | 15 ++---- src/SMAPI.Mods.ConsoleCommands/ModEntry.cs | 31 +++++++++--- 9 files changed, 134 insertions(+), 64 deletions(-) create mode 100644 src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/TestInputCommand.cs (limited to 'src/SMAPI.Mods.ConsoleCommands/Framework') diff --git a/docs/release-notes.md b/docs/release-notes.md index b7bd7b53..8dac1d0c 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -10,6 +10,9 @@ * Fixed update-check error if a mod's Chucklefish page has no version. * Fixed SMAPI beta versions not showing update alert on next launch (thanks to danvolchek!). +For the Console Commands mod: + * Added `test_input` command to view button codes in the console. + For modders: * Asset propagation for player sprites now affects other players' sprites, and updates recolor maps (e.g. sleeves). * Removed invalid-schedule validation which had false positives. @@ -31,13 +34,14 @@ Released 05 January 2019 for Stardew Valley 1.4 or later. * Fixed compatibility with Linux Mint 18 (thanks to techge!), Arch Linux, and Linux systems with libhybris-utils installed. * Fixed memory leak when repeatedly loading a save and returning to title. * Fixed memory leak when mods reload assets. - * Fixes for Console Commands mod: - * added new clothing items; - * fixed spawning new flooring and rings (thanks to Mizzion!); - * fixed spawning custom rings added by mods; - * Fixed errors when some item data is invalid. * Updated translations. Thanks to L30Bola (added Portuguese), PlussRolf (added Spanish), and shirutan (added Japanese)! +* For the Console Commands mod: + * Added new clothing items. + * Fixed spawning new flooring and rings (thanks to Mizzion!). + * Fixed spawning custom rings added by mods. + * Fixed errors when some item data is invalid. + * For the web UI: * Added option to edit & reupload in the JSON validator. * File uploads are now stored in Azure storage instead of Pastebin, due to ongoing Pastebin perfomance issues. diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/ITrainerCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/ITrainerCommand.cs index a0b739f8..d4d36e5d 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/ITrainerCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/ITrainerCommand.cs @@ -12,8 +12,11 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands /// The command description. string Description { get; } - /// Whether the command needs to perform logic when the game updates. - bool NeedsUpdate { get; } + /// Whether the command may need to perform logic when the game updates. This value shouldn't change. + bool MayNeedUpdate { get; } + + /// Whether the command may need to perform logic when the player presses a button. This value shouldn't change. + bool MayNeedInput { get; } /********* @@ -27,6 +30,11 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands /// Perform any logic needed on update tick. /// Writes messages to the console and log file. - void Update(IMonitor monitor); + void OnUpdated(IMonitor monitor); + + /// Perform any logic when input is received. + /// Writes messages to the console and log file. + /// The button that was pressed. + void OnButtonPressed(IMonitor monitor, SButton button); } } diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/TestInputCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/TestInputCommand.cs new file mode 100644 index 00000000..11aa10c3 --- /dev/null +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/TestInputCommand.cs @@ -0,0 +1,59 @@ +using System; + +namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other +{ + /// A command which logs the keys being pressed for 30 seconds once enabled. + internal class TestInputCommand : TrainerCommand + { + /********* + ** Fields + *********/ + /// The number of seconds for which to log input. + private readonly int LogSeconds = 30; + + /// When the command should stop printing input, or null if currently disabled. + private long? ExpiryTicks; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + public TestInputCommand() + : base("test_input", "Prints all input to the console for 30 seconds.", mayNeedUpdate: true, mayNeedInput: true) { } + + /// Handle the command. + /// Writes messages to the console and log file. + /// The command name. + /// The command arguments. + public override void Handle(IMonitor monitor, string command, ArgumentParser args) + { + this.ExpiryTicks = DateTime.UtcNow.Add(TimeSpan.FromSeconds(this.LogSeconds)).Ticks; + monitor.Log($"OK, logging all player input for {this.LogSeconds} seconds.", LogLevel.Info); + } + + /// Perform any logic needed on update tick. + /// Writes messages to the console and log file. + public override void OnUpdated(IMonitor monitor) + { + // handle expiry + if (this.ExpiryTicks == null) + return; + if (this.ExpiryTicks <= DateTime.UtcNow.Ticks) + { + monitor.Log("No longer logging input.", LogLevel.Info); + this.ExpiryTicks = null; + return; + } + } + + /// Perform any logic when input is received. + /// Writes messages to the console and log file. + /// The button that was pressed. + public override void OnButtonPressed(IMonitor monitor, SButton button) + { + if (this.ExpiryTicks != null) + monitor.Log($"Pressed {button}", LogLevel.Info); + } + } +} diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetHealthCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetHealthCommand.cs index 1abb82b5..59bda5dd 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetHealthCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetHealthCommand.cs @@ -1,4 +1,4 @@ -using System.Linq; +using System.Linq; using StardewValley; namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player @@ -13,19 +13,12 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player private bool InfiniteHealth; - /********* - ** Accessors - *********/ - /// Whether the command needs to perform logic when the game updates. - public override bool NeedsUpdate => this.InfiniteHealth; - - /********* ** Public methods *********/ /// Construct an instance. public SetHealthCommand() - : base("player_sethealth", "Sets the player's health.\n\nUsage: player_sethealth [value]\n- value: an integer amount, or 'inf' for infinite health.") { } + : base("player_sethealth", "Sets the player's health.\n\nUsage: player_sethealth [value]\n- value: an integer amount, or 'inf' for infinite health.", mayNeedUpdate: true) { } /// Handle the command. /// Writes messages to the console and log file. @@ -62,9 +55,9 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player /// Perform any logic needed on update tick. /// Writes messages to the console and log file. - public override void Update(IMonitor monitor) + public override void OnUpdated(IMonitor monitor) { - if (this.InfiniteHealth) + if (this.InfiniteHealth && Context.IsWorldReady) Game1.player.health = Game1.player.maxHealth; } } diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetMoneyCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetMoneyCommand.cs index 1706bbc1..6e3d68b6 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetMoneyCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetMoneyCommand.cs @@ -13,19 +13,12 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player private bool InfiniteMoney; - /********* - ** Accessors - *********/ - /// Whether the command needs to perform logic when the game updates. - public override bool NeedsUpdate => this.InfiniteMoney; - - /********* ** Public methods *********/ /// Construct an instance. public SetMoneyCommand() - : base("player_setmoney", "Sets the player's money.\n\nUsage: player_setmoney \n- value: an integer amount, or 'inf' for infinite money.") { } + : base("player_setmoney", "Sets the player's money.\n\nUsage: player_setmoney \n- value: an integer amount, or 'inf' for infinite money.", mayNeedUpdate: true) { } /// Handle the command. /// Writes messages to the console and log file. @@ -62,9 +55,9 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player /// Perform any logic needed on update tick. /// Writes messages to the console and log file. - public override void Update(IMonitor monitor) + public override void OnUpdated(IMonitor monitor) { - if (this.InfiniteMoney) + if (this.InfiniteMoney && Context.IsWorldReady) Game1.player.Money = 999999; } } diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetStaminaCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetStaminaCommand.cs index 009cb1de..60a1dcb1 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetStaminaCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetStaminaCommand.cs @@ -1,4 +1,4 @@ -using System.Linq; +using System.Linq; using StardewValley; namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player @@ -13,19 +13,12 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player private bool InfiniteStamina; - /********* - ** Accessors - *********/ - /// Whether the command needs to perform logic when the game updates. - public override bool NeedsUpdate => this.InfiniteStamina; - - /********* ** Public methods *********/ /// Construct an instance. public SetStaminaCommand() - : base("player_setstamina", "Sets the player's stamina.\n\nUsage: player_setstamina [value]\n- value: an integer amount, or 'inf' for infinite stamina.") { } + : base("player_setstamina", "Sets the player's stamina.\n\nUsage: player_setstamina [value]\n- value: an integer amount, or 'inf' for infinite stamina.", mayNeedUpdate: true) { } /// Handle the command. /// Writes messages to the console and log file. @@ -62,9 +55,9 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player /// Perform any logic needed on update tick. /// Writes messages to the console and log file. - public override void Update(IMonitor monitor) + public override void OnUpdated(IMonitor monitor) { - if (this.InfiniteStamina) + if (this.InfiniteStamina && Context.IsWorldReady) Game1.player.stamina = Game1.player.MaxStamina; } } diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/TrainerCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/TrainerCommand.cs index 466b8f6e..6d5cae97 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/TrainerCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/TrainerCommand.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; @@ -16,8 +16,11 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands /// The command description. public string Description { get; } - /// Whether the command needs to perform logic when the game updates. - public virtual bool NeedsUpdate { get; } = false; + /// Whether the command may need to perform logic when the player presses a button. This value shouldn't change. + public bool MayNeedInput { get; } + + /// Whether the command may need to perform logic when the game updates. This value shouldn't change. + public bool MayNeedUpdate { get; } /********* @@ -31,7 +34,12 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands /// Perform any logic needed on update tick. /// Writes messages to the console and log file. - public virtual void Update(IMonitor monitor) { } + public virtual void OnUpdated(IMonitor monitor) { } + + /// Perform any logic when input is received. + /// Writes messages to the console and log file. + /// The button that was pressed. + public virtual void OnButtonPressed(IMonitor monitor, SButton button) { } /********* @@ -40,10 +48,14 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands /// Construct an instance. /// The command name the user must type. /// The command description. - protected TrainerCommand(string name, string description) + /// Whether the command may need to perform logic when the player presses a button. + /// Whether the command may need to perform logic when the game updates. + protected TrainerCommand(string name, string description, bool mayNeedInput = false, bool mayNeedUpdate = false) { this.Name = name; this.Description = description; + this.MayNeedInput = mayNeedInput; + this.MayNeedUpdate = mayNeedUpdate; } /// Log an error indicating incorrect usage. diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/FreezeTimeCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/FreezeTimeCommand.cs index 6a7ab162..736a93a0 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/FreezeTimeCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/FreezeTimeCommand.cs @@ -1,4 +1,4 @@ -using System.Linq; +using System.Linq; using StardewValley; namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World @@ -16,19 +16,12 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World private bool FreezeTime; - /********* - ** Accessors - *********/ - /// Whether the command needs to perform logic when the game updates. - public override bool NeedsUpdate => this.FreezeTime; - - /********* ** Public methods *********/ /// Construct an instance. public FreezeTimeCommand() - : base("world_freezetime", "Freezes or resumes time.\n\nUsage: world_freezetime [value]\n- value: one of 0 (resume), 1 (freeze), or blank (toggle).") { } + : base("world_freezetime", "Freezes or resumes time.\n\nUsage: world_freezetime [value]\n- value: one of 0 (resume), 1 (freeze), or blank (toggle).", mayNeedUpdate: true) { } /// Handle the command. /// Writes messages to the console and log file. @@ -57,9 +50,9 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World /// Perform any logic needed on update tick. /// Writes messages to the console and log file. - public override void Update(IMonitor monitor) + public override void OnUpdated(IMonitor monitor) { - if (this.FreezeTime) + if (this.FreezeTime && Context.IsWorldReady) Game1.timeOfDay = FreezeTimeCommand.FrozenTime; } } diff --git a/src/SMAPI.Mods.ConsoleCommands/ModEntry.cs b/src/SMAPI.Mods.ConsoleCommands/ModEntry.cs index 4807c46d..5c4f3bba 100644 --- a/src/SMAPI.Mods.ConsoleCommands/ModEntry.cs +++ b/src/SMAPI.Mods.ConsoleCommands/ModEntry.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using StardewModdingAPI.Events; using StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands; namespace StardewModdingAPI.Mods.ConsoleCommands @@ -14,6 +15,12 @@ namespace StardewModdingAPI.Mods.ConsoleCommands /// The commands to handle. private ITrainerCommand[] Commands; + /// The commands which may need to handle update ticks. + private ITrainerCommand[] UpdateHandlers; + + /// The commands which may need to handle input. + private ITrainerCommand[] InputHandlers; + /********* ** Public methods @@ -27,27 +34,35 @@ namespace StardewModdingAPI.Mods.ConsoleCommands foreach (ITrainerCommand command in this.Commands) helper.ConsoleCommands.Add(command.Name, command.Description, (name, args) => this.HandleCommand(command, name, args)); + // cache commands + this.InputHandlers = this.Commands.Where(p => p.MayNeedInput).ToArray(); + this.UpdateHandlers = this.Commands.Where(p => p.MayNeedUpdate).ToArray(); + // hook events helper.Events.GameLoop.UpdateTicked += this.OnUpdateTicked; + helper.Events.Input.ButtonPressed += this.OnButtonPressed; } /********* ** Private methods *********/ + /// The method invoked when a button is pressed. + /// The event sender. + /// The event arguments. + private void OnButtonPressed(object sender, ButtonPressedEventArgs e) + { + foreach (ITrainerCommand command in this.InputHandlers) + command.OnButtonPressed(this.Monitor, e.Button); + } + /// The method invoked when the game updates its state. /// The event sender. /// The event arguments. private void OnUpdateTicked(object sender, EventArgs e) { - if (!Context.IsWorldReady) - return; - - foreach (ITrainerCommand command in this.Commands) - { - if (command.NeedsUpdate) - command.Update(this.Monitor); - } + foreach (ITrainerCommand command in this.UpdateHandlers) + command.OnUpdated(this.Monitor); } /// Handle a console command. -- cgit From 22a0a32b6d959946bfd80bf0ca9796378f36e0cd Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 26 Jan 2020 19:49:17 -0500 Subject: refactor performance counter code This commit performs some general refactoring, including... - avoid manually duplicating the event list; - rework the 'is important' event flag; - remove the new packages (Cyotek.Collections can be replaced with built-in types, and System.ValueTuple won't work in the Mono version used on Linux/Mac); - improve performance; - minor cleanup. --- build/common.targets | 1 - build/prepare-install-package.targets | 1 - .../Framework/Commands/ArgumentParser.cs | 22 +- .../Commands/Other/PerformanceCounterCommand.cs | 655 ++++++++++----------- .../Framework/Commands/TrainerCommand.cs | 18 +- .../SMAPI.Mods.ConsoleCommands.csproj | 4 - src/SMAPI/Constants.cs | 3 +- src/SMAPI/Framework/Events/EventManager.cs | 48 +- src/SMAPI/Framework/Events/IManagedEvent.cs | 10 +- src/SMAPI/Framework/Events/ManagedEvent.cs | 71 +-- .../Framework/PerformanceCounter/AlertContext.cs | 14 +- .../Framework/PerformanceCounter/AlertEntry.cs | 31 +- .../EventPerformanceCounterCollection.cs | 16 - .../Framework/PerformanceCounter/PeakEntry.cs | 25 +- .../PerformanceCounter/PerformanceCounter.cs | 143 ++--- .../PerformanceCounterCollection.cs | 196 +++--- .../PerformanceCounter/PerformanceCounterEntry.cs | 26 +- .../PerformanceCounterManager.cs | 212 +++---- src/SMAPI/Framework/SCore.cs | 15 +- src/SMAPI/Framework/SGame.cs | 11 +- src/SMAPI/Program.cs | 2 +- src/SMAPI/SMAPI.csproj | 5 - 22 files changed, 706 insertions(+), 823 deletions(-) delete mode 100644 src/SMAPI/Framework/PerformanceCounter/EventPerformanceCounterCollection.cs (limited to 'src/SMAPI.Mods.ConsoleCommands/Framework') diff --git a/build/common.targets b/build/common.targets index 78b435d0..df2d4861 100644 --- a/build/common.targets +++ b/build/common.targets @@ -32,7 +32,6 @@ - diff --git a/build/prepare-install-package.targets b/build/prepare-install-package.targets index 96716ecb..61b12039 100644 --- a/build/prepare-install-package.targets +++ b/build/prepare-install-package.targets @@ -41,7 +41,6 @@ - diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/ArgumentParser.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/ArgumentParser.cs index 40691a3e..9c7082c9 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/ArgumentParser.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/ArgumentParser.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections; using System.Collections.Generic; using System.Globalization; @@ -32,13 +32,6 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands /// The zero-based index of the element to get. public string this[int index] => this.Args[index]; - /// A method which parses a string argument into the given value. - /// The expected argument type. - /// The argument to parse. - /// The parsed value. - /// Returns whether the argument was successfully parsed. - public delegate bool ParseDelegate(string input, out T output); - /********* ** Public methods @@ -114,19 +107,6 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands return true; } - public bool IsDecimal(int index) - { - if (!this.TryGet(index, "", out string raw, false)) - return false; - - if (!decimal.TryParse(raw, NumberStyles.Number, CultureInfo.InvariantCulture, out decimal value)) - { - return false; - } - - return true; - } - /// Try to read a decimal argument. /// The argument index. /// The argument name for error messages. diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/PerformanceCounterCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/PerformanceCounterCommand.cs index 2260296b..820f1939 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/PerformanceCounterCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/PerformanceCounterCommand.cs @@ -7,24 +7,13 @@ using StardewModdingAPI.Framework.PerformanceCounter; namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other { - // ReSharper disable once UnusedType.Global + /// A set of commands which displays or configures performance monitoring. internal class PerformanceCounterCommand : TrainerCommand { - /// The command names and aliases - private readonly Dictionary SubCommandNames = new Dictionary() - { - {SubCommand.Summary, new[] {"summary", "sum", "s"}}, - {SubCommand.Detail, new[] {"detail", "d"}}, - {SubCommand.Reset, new[] {"reset", "r"}}, - {SubCommand.Trigger, new[] {"trigger"}}, - {SubCommand.Enable, new[] {"enable"}}, - {SubCommand.Disable, new[] {"disable"}}, - {SubCommand.Examples, new[] {"examples"}}, - {SubCommand.Concepts, new[] {"concepts"}}, - {SubCommand.Help, new[] {"help"}}, - }; - - /// The available commands enum + /********* + ** Fields + *********/ + /// The available commands. private enum SubCommand { Summary, @@ -33,16 +22,16 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other Trigger, Enable, Disable, - Examples, - Help, - Concepts, - None + Help } + + /********* + ** Public methods + *********/ /// Construct an instance. - public PerformanceCounterCommand() : base("pc", PerformanceCounterCommand.GetDescription()) - { - } + public PerformanceCounterCommand() + : base("performance", PerformanceCounterCommand.GetDescription()) { } /// Handle the command. /// Writes messages to the console and log file. @@ -50,92 +39,94 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other /// The command arguments. public override void Handle(IMonitor monitor, string command, ArgumentParser args) { - if (args.TryGet(0, "command", out string subCommandString, false)) + // parse args + SubCommand subcommand = SubCommand.Summary; { - SubCommand subSubCommand = this.ParseCommandString(subCommandString); - - switch (subSubCommand) + if (args.TryGet(0, "command", out string subcommandStr, false) && !Enum.TryParse(subcommandStr, ignoreCase: true, out subcommand)) { - case SubCommand.Summary: - this.HandleSummarySubCommand(monitor, args); - break; - case SubCommand.Detail: - this.HandleDetailSubCommand(monitor, args); - break; - case SubCommand.Reset: - this.HandleResetSubCommand(monitor, args); - break; - case SubCommand.Trigger: - this.HandleTriggerSubCommand(monitor, args); - break; - case SubCommand.Examples: - break; - case SubCommand.Concepts: - this.OutputHelp(monitor, SubCommand.Concepts); - break; - case SubCommand.Enable: - SCore.PerformanceCounterManager.EnableTracking = true; - monitor.Log("Performance counter tracking is now enabled", LogLevel.Info); - break; - case SubCommand.Disable: - SCore.PerformanceCounterManager.EnableTracking = false; - monitor.Log("Performance counter tracking is now disabled", LogLevel.Info); - break; - case SubCommand.Help: - if (args.TryGet(1, "command", out string commandString)) - this.OutputHelp(monitor, this.ParseCommandString(commandString)); - break; - default: - this.LogUsageError(monitor, $"Unknown command {subCommandString}"); - break; + this.LogUsageError(monitor, $"Unknown command {subcommandStr}"); + return; } } - else - this.HandleSummarySubCommand(monitor, args); + + // handle + switch (subcommand) + { + case SubCommand.Summary: + this.HandleSummarySubCommand(monitor, args); + break; + + case SubCommand.Detail: + this.HandleDetailSubCommand(monitor, args); + break; + + case SubCommand.Reset: + this.HandleResetSubCommand(monitor, args); + break; + + case SubCommand.Trigger: + this.HandleTriggerSubCommand(monitor, args); + break; + + case SubCommand.Enable: + SCore.PerformanceMonitor.EnableTracking = true; + monitor.Log("Performance counter tracking is now enabled", LogLevel.Info); + break; + + case SubCommand.Disable: + SCore.PerformanceMonitor.EnableTracking = false; + monitor.Log("Performance counter tracking is now disabled", LogLevel.Info); + break; + + case SubCommand.Help: + this.OutputHelp(monitor, args.TryGet(1, "command", out _) ? subcommand : null as SubCommand?); + break; + + default: + this.LogUsageError(monitor, $"Unknown command {subcommand}"); + break; + } } + + /********* + ** Private methods + *********/ /// Handles the summary sub command. /// Writes messages to the console and log file. /// The command arguments. private void HandleSummarySubCommand(IMonitor monitor, ArgumentParser args) { - IEnumerable data; - if (!args.TryGet(1, "mode", out string mode, false)) - { mode = "important"; - } + IEnumerable data = SCore.PerformanceMonitor.GetCollections(); switch (mode) { case null: case "important": - data = SCore.PerformanceCounterManager.PerformanceCounterCollections.Where(p => p.IsImportant); + data = data.Where(p => p.IsPerformanceCritical); break; + case "all": - data = SCore.PerformanceCounterManager.PerformanceCounterCollections; break; + default: - data = SCore.PerformanceCounterManager.PerformanceCounterCollections.Where(p => - p.Name.ToLowerInvariant().Contains(mode.ToLowerInvariant())); + data = data.Where(p => p.Name.ToLowerInvariant().Contains(mode.ToLowerInvariant())); break; } double? threshold = null; - if (args.TryGetDecimal(2, "threshold", out decimal t, false)) - { - threshold = (double?) t; - } - - StringBuilder sb = new StringBuilder(); + threshold = (double?)t; TimeSpan interval = TimeSpan.FromSeconds(60); - sb.AppendLine($"Summary over the last {interval.TotalSeconds} seconds:"); - sb.AppendLine(this.GetTableString( + StringBuilder report = new StringBuilder(); + report.AppendLine($"Summary over the last {interval.TotalSeconds} seconds:"); + report.AppendLine(this.GetTableString( data: data, - header: new[] {"Collection", "Avg Calls/s", "Avg Exec Time (Game)", "Avg Exec Time (Mods)", "Avg Exec Time (Game+Mods)", "Peak Exec Time"}, + header: new[] { "Collection", "Avg Calls/s", "Avg Exec Time (Game)", "Avg Exec Time (Mods)", "Avg Exec Time (Game+Mods)", "Peak Exec Time" }, getRow: item => new[] { item.Name, @@ -148,7 +139,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other true )); - monitor.Log(sb.ToString(), LogLevel.Info); + monitor.Log(report.ToString(), LogLevel.Info); } /// Handles the detail sub command. @@ -163,26 +154,16 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other if (args.TryGet(1, "collection", out string collectionName)) { - collections.AddRange(SCore.PerformanceCounterManager.PerformanceCounterCollections.Where( - collection => collection.Name.ToLowerInvariant().Contains(collectionName.ToLowerInvariant()))); + collections.AddRange(SCore.PerformanceMonitor.GetCollections().Where(collection => collection.Name.ToLowerInvariant().Contains(collectionName.ToLowerInvariant()))); - if (args.IsDecimal(2) && args.TryGetDecimal(2, "threshold", out decimal value, false)) - { - thresholdMilliseconds = (double?) value; - } - else - { - if (args.TryGet(2, "source", out string sourceName, false)) - { - sourceFilter = sourceName; - } - } + if (args.Count >= 2 && decimal.TryParse(args[2], out _) && args.TryGetDecimal(2, "threshold", out decimal value, false)) + thresholdMilliseconds = (double?)value; + else if (args.TryGet(2, "source", out string sourceName, false)) + sourceFilter = sourceName; } foreach (PerformanceCounterCollection c in collections) - { this.OutputPerformanceCollectionDetail(monitor, c, averageInterval, thresholdMilliseconds, sourceFilter); - } } /// Handles the trigger sub command. @@ -197,46 +178,44 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other case "list": this.OutputAlertTriggers(monitor); break; + case "collection": if (args.TryGet(2, "name", out string collectionName)) { if (args.TryGetDecimal(3, "threshold", out decimal threshold)) { - if (args.TryGet(4, "source", out string source, false)) - { - this.ConfigureAlertTrigger(monitor, collectionName, source, threshold); - } - else - { - this.ConfigureAlertTrigger(monitor, collectionName, null, threshold); - } + if (!args.TryGet(4, "source", out string source, required: false)) + source = null; + this.ConfigureAlertTrigger(monitor, collectionName, source, threshold); } } break; + case "pause": - SCore.PerformanceCounterManager.PauseAlerts = true; - monitor.Log($"Alerts are now paused.", LogLevel.Info); + SCore.PerformanceMonitor.PauseAlerts = true; + monitor.Log("Alerts are now paused.", LogLevel.Info); break; + case "resume": - SCore.PerformanceCounterManager.PauseAlerts = false; - monitor.Log($"Alerts are now resumed.", LogLevel.Info); + SCore.PerformanceMonitor.PauseAlerts = false; + monitor.Log("Alerts are now resumed.", LogLevel.Info); break; + case "dump": this.OutputAlertTriggers(monitor, true); break; + case "clear": this.ClearAlertTriggers(monitor); break; + default: this.LogUsageError(monitor, $"Unknown mode {mode}. See 'pc help trigger' for usage."); break; } - } else - { this.OutputAlertTriggers(monitor); - } } /// Sets up an an alert trigger. @@ -246,7 +225,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other /// The trigger threshold, or 0 to remove. private void ConfigureAlertTrigger(IMonitor monitor, string collectionName, string sourceName, decimal threshold) { - foreach (PerformanceCounterCollection collection in SCore.PerformanceCounterManager.PerformanceCounterCollections) + foreach (PerformanceCounterCollection collection in SCore.PerformanceMonitor.GetCollections()) { if (collection.Name.ToLowerInvariant().Equals(collectionName.ToLowerInvariant())) { @@ -255,8 +234,8 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other if (threshold != 0) { collection.EnableAlerts = true; - collection.AlertThresholdMilliseconds = (double) threshold; - monitor.Log($"Set up alert triggering for '{collectionName}' with '{this.FormatMilliseconds((double?) threshold)}'", LogLevel.Info); + collection.AlertThresholdMilliseconds = (double)threshold; + monitor.Log($"Set up alert triggering for '{collectionName}' with '{this.FormatMilliseconds((double?)threshold)}'", LogLevel.Info); } else { @@ -275,14 +254,11 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other if (threshold != 0) { performanceCounter.Value.EnableAlerts = true; - performanceCounter.Value.AlertThresholdMilliseconds = (double) threshold; - monitor.Log($"Set up alert triggering for '{sourceName}' in collection '{collectionName}' with '{this.FormatMilliseconds((double?) threshold)}", LogLevel.Info); + performanceCounter.Value.AlertThresholdMilliseconds = (double)threshold; + monitor.Log($"Set up alert triggering for '{sourceName}' in collection '{collectionName}' with '{this.FormatMilliseconds((double?)threshold)}", LogLevel.Info); } else - { performanceCounter.Value.EnableAlerts = false; - } - return; } } @@ -302,7 +278,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other private void ClearAlertTriggers(IMonitor monitor) { int clearedTriggers = 0; - foreach (PerformanceCounterCollection collection in SCore.PerformanceCounterManager.PerformanceCounterCollections) + foreach (PerformanceCounterCollection collection in SCore.PerformanceMonitor.GetCollections()) { if (collection.EnableAlerts) { @@ -329,92 +305,75 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other /// True to dump the triggers as commands. private void OutputAlertTriggers(IMonitor monitor, bool asDump = false) { - StringBuilder sb = new StringBuilder(); - sb.AppendLine("Configured triggers:"); - sb.AppendLine(); - var collectionTriggers = new List<(string collectionName, double threshold)>(); - var sourceTriggers = new List<(string collectionName, string sourceName, double threshold)>(); + StringBuilder report = new StringBuilder(); + report.AppendLine("Configured triggers:"); + report.AppendLine(); + var collectionTriggers = new List(); + var sourceTriggers = new List(); - foreach (PerformanceCounterCollection collection in SCore.PerformanceCounterManager.PerformanceCounterCollections) + foreach (PerformanceCounterCollection collection in SCore.PerformanceMonitor.GetCollections()) { if (collection.EnableAlerts) - { - collectionTriggers.Add((collection.Name, collection.AlertThresholdMilliseconds)); - } + collectionTriggers.Add(new CollectionTrigger(collection.Name, collection.AlertThresholdMilliseconds)); - sourceTriggers.AddRange(from performanceCounter in - collection.PerformanceCounters where performanceCounter.Value.EnableAlerts - select (collection.Name, performanceCounter.Value.Source, performanceCounter.Value.AlertThresholdMilliseconds)); + sourceTriggers.AddRange( + from counter in collection.PerformanceCounters + where counter.Value.EnableAlerts + select new SourceTrigger(collection.Name, counter.Value.Source, counter.Value.AlertThresholdMilliseconds) + ); } if (collectionTriggers.Count > 0) { - sb.AppendLine("Collection Triggers:"); - sb.AppendLine(); + report.AppendLine("Collection Triggers:"); + report.AppendLine(); if (asDump) { foreach (var item in collectionTriggers) - { - sb.AppendLine($"pc trigger {item.collectionName} {item.threshold}"); - } + report.AppendLine($"pc trigger {item.CollectionName} {item.Threshold}"); } else { - sb.AppendLine(this.GetTableString( + report.AppendLine(this.GetTableString( data: collectionTriggers, - header: new[] {"Collection", "Threshold"}, - getRow: item => new[] - { - item.collectionName, - this.FormatMilliseconds(item.threshold) - }, + header: new[] { "Collection", "Threshold" }, + getRow: item => new[] { item.CollectionName, this.FormatMilliseconds(item.Threshold) }, true )); } - sb.AppendLine(); + report.AppendLine(); } else - { - sb.AppendLine("No collection triggers."); - } + report.AppendLine("No collection triggers."); if (sourceTriggers.Count > 0) { - sb.AppendLine("Source Triggers:"); - sb.AppendLine(); + report.AppendLine("Source Triggers:"); + report.AppendLine(); if (asDump) { - foreach (var item in sourceTriggers) - { - sb.AppendLine($"pc trigger {item.collectionName} {item.threshold} {item.sourceName}"); - } + foreach (SourceTrigger item in sourceTriggers) + report.AppendLine($"pc trigger {item.CollectionName} {item.Threshold} {item.SourceName}"); } else { - sb.AppendLine(this.GetTableString( + report.AppendLine(this.GetTableString( data: sourceTriggers, - header: new[] {"Collection", "Source", "Threshold"}, - getRow: item => new[] - { - item.collectionName, - item.sourceName, - this.FormatMilliseconds(item.threshold) - }, + header: new[] { "Collection", "Source", "Threshold" }, + getRow: item => new[] { item.CollectionName, item.SourceName, this.FormatMilliseconds(item.Threshold) }, true )); } - sb.AppendLine(); + report.AppendLine(); } else - { - sb.AppendLine("No source triggers."); - } + report.AppendLine("No source triggers."); - monitor.Log(sb.ToString(), LogLevel.Info); + monitor.Log(report.ToString(), LogLevel.Info); } /// Handles the reset sub command. @@ -422,25 +381,25 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other /// The command arguments. private void HandleResetSubCommand(IMonitor monitor, ArgumentParser args) { - if (args.TryGet(1, "type", out string type, false, new []{"category", "source"})) + if (args.TryGet(1, "type", out string type, false, new[] { "category", "source" })) { args.TryGet(2, "name", out string name); switch (type) { case "category": - SCore.PerformanceCounterManager.ResetCollection(name); + SCore.PerformanceMonitor.ResetCollection(name); monitor.Log($"All performance counters for category {name} are now cleared.", LogLevel.Info); break; case "source": - SCore.PerformanceCounterManager.ResetSource(name); + SCore.PerformanceMonitor.ResetSource(name); monitor.Log($"All performance counters for source {name} are now cleared.", LogLevel.Info); break; } } else { - SCore.PerformanceCounterManager.Reset(); + SCore.PerformanceMonitor.Reset(); monitor.Log("All performance counters are now cleared.", LogLevel.Info); } } @@ -455,7 +414,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other private void OutputPerformanceCollectionDetail(IMonitor monitor, PerformanceCounterCollection collection, TimeSpan averageInterval, double? thresholdMilliseconds, string sourceFilter = null) { - StringBuilder sb = new StringBuilder($"Performance Counter for {collection.Name}:\n\n"); + StringBuilder report = new StringBuilder($"Performance Counter for {collection.Name}:\n\n"); List> data = collection.PerformanceCounters.ToList(); @@ -466,15 +425,13 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other } if (thresholdMilliseconds != null) - { data = data.Where(p => p.Value.GetAverage(averageInterval) >= thresholdMilliseconds).ToList(); - } if (data.Any()) { - sb.AppendLine(this.GetTableString( + report.AppendLine(this.GetTableString( data: data, - header: new[] {"Mod", $"Avg Exec Time (last {(int) averageInterval.TotalSeconds}s)", "Last Exec Time", "Peak Exec Time", $"Peak Exec Time (last {(int) averageInterval.TotalSeconds}s)"}, + header: new[] { "Mod", $"Avg Exec Time (last {(int)averageInterval.TotalSeconds}s)", "Last Exec Time", "Peak Exec Time", $"Peak Exec Time (last {(int)averageInterval.TotalSeconds}s)" }, getRow: item => new[] { item.Key, @@ -488,28 +445,13 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other } else { - sb.Clear(); - sb.AppendLine($"Performance Counter for {collection.Name}: none."); - } - - monitor.Log(sb.ToString(), LogLevel.Info); - } - - /// Parses a command string and returns the associated command. - /// The command string - /// The parsed command. - private SubCommand ParseCommandString(string commandString) - { - foreach (var i in this.SubCommandNames.Where(i => - i.Value.Any(str => str.Equals(commandString, StringComparison.InvariantCultureIgnoreCase)))) - { - return i.Key; + report.Clear(); + report.AppendLine($"Performance Counter for {collection.Name}: none."); } - return SubCommand.None; + monitor.Log(report.ToString(), LogLevel.Info); } - /// Formats the given milliseconds value into a string format. Optionally /// allows a threshold to return "-" if the value is less than the threshold. /// The milliseconds to format. Returns "-" if null @@ -518,175 +460,212 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other private string FormatMilliseconds(double? milliseconds, double? thresholdMilliseconds = null) { if (milliseconds == null || (thresholdMilliseconds != null && milliseconds < thresholdMilliseconds)) - { return "-"; - } - return ((double) milliseconds).ToString("F2"); + return ((double)milliseconds).ToString("F2"); } /// Shows detailed help for a specific sub command. - /// The output monitor - /// The sub command - private void OutputHelp(IMonitor monitor, SubCommand subCommand) + /// The output monitor. + /// The subcommand. + private void OutputHelp(IMonitor monitor, SubCommand? subcommand) { - StringBuilder sb = new StringBuilder(); - sb.AppendLine(); + StringBuilder report = new StringBuilder(); + report.AppendLine(); - switch (subCommand) + switch (subcommand) { - case SubCommand.Concepts: - sb.AppendLine("A performance counter is a metric which measures execution time. Each performance"); - sb.AppendLine("counter consists of:"); - sb.AppendLine(); - sb.AppendLine(" - A source, which typically is a mod or the game itself."); - sb.AppendLine(" - A ring buffer which stores the data points (execution time and time when it was executed)"); - sb.AppendLine(); - sb.AppendLine("A set of performance counters is organized in a collection to group various areas."); - sb.AppendLine("Per default, collections for all game events [1] are created."); - sb.AppendLine(); - sb.AppendLine("Example:"); - sb.AppendLine(); - sb.AppendLine("The performance counter collection named 'Display.Rendered' contains one performance"); - sb.AppendLine("counters when the game executes the 'Display.Rendered' event, and one additional"); - sb.AppendLine("performance counter for each mod which handles the 'Display.Rendered' event."); - sb.AppendLine(); - sb.AppendLine("[1] https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Events"); - break; case SubCommand.Detail: - sb.AppendLine("Usage: pc detail "); - sb.AppendLine(" pc detail "); - sb.AppendLine(); - sb.AppendLine("Displays details for a specific collection."); - sb.AppendLine(); - sb.AppendLine("Arguments:"); - sb.AppendLine(" Required. The full or partial name of the collection to display."); - sb.AppendLine(" Optional. The full or partial name of the source."); - sb.AppendLine(" Optional. The threshold in milliseconds. Any average execution time below that"); - sb.AppendLine(" threshold is not reported."); - sb.AppendLine(); - sb.AppendLine("Examples:"); - sb.AppendLine("pc detail Display.Rendering Displays all performance counters for the 'Display.Rendering' collection"); - sb.AppendLine("pc detail Display.Rendering Pathoschild.ChestsAnywhere Displays the 'Display.Rendering' performance counter for 'Pathoschild.ChestsAnywhere'"); - sb.AppendLine("pc detail Display.Rendering 5 Displays the 'Display.Rendering' performance counters exceeding an average of 5ms"); + report.AppendLine("Usage: pc detail "); + report.AppendLine(" pc detail "); + report.AppendLine(); + report.AppendLine("Displays details for a specific collection."); + report.AppendLine(); + report.AppendLine("Arguments:"); + report.AppendLine(" Required. The full or partial name of the collection to display."); + report.AppendLine(" Optional. The full or partial name of the source."); + report.AppendLine(" Optional. The threshold in milliseconds. Any average execution time below that"); + report.AppendLine(" threshold is not reported."); + report.AppendLine(); + report.AppendLine("Examples:"); + report.AppendLine("pc detail Display.Rendering Displays all performance counters for the 'Display.Rendering' collection"); + report.AppendLine("pc detail Display.Rendering Pathoschild.ChestsAnywhere Displays the 'Display.Rendering' performance counter for 'Pathoschild.ChestsAnywhere'"); + report.AppendLine("pc detail Display.Rendering 5 Displays the 'Display.Rendering' performance counters exceeding an average of 5ms"); break; + case SubCommand.Summary: - sb.AppendLine("Usage: pc summary "); - sb.AppendLine(); - sb.AppendLine("Displays the performance counter summary."); - sb.AppendLine(); - sb.AppendLine("Arguments:"); - sb.AppendLine(" Optional. Defaults to 'important' if omitted. Specifies one of these modes:"); - sb.AppendLine(" - all Displays performance counters from all collections"); - sb.AppendLine(" - important Displays only important performance counter collections"); - sb.AppendLine(); - sb.AppendLine(" Optional. Only shows performance counter collections matching the given name"); - sb.AppendLine(" Optional. Hides the actual execution time if it is below this threshold"); - sb.AppendLine(); - sb.AppendLine("Examples:"); - sb.AppendLine("pc summary all Shows all events"); - sb.AppendLine("pc summary all 5 Shows all events"); - sb.AppendLine("pc summary Display.Rendering Shows only the 'Display.Rendering' collection"); + report.AppendLine("Usage: pc summary "); + report.AppendLine(); + report.AppendLine("Displays the performance counter summary."); + report.AppendLine(); + report.AppendLine("Arguments:"); + report.AppendLine(" Optional. Defaults to 'important' if omitted. Specifies one of these modes:"); + report.AppendLine(" - all Displays performance counters from all collections"); + report.AppendLine(" - important Displays only important performance counter collections"); + report.AppendLine(); + report.AppendLine(" Optional. Only shows performance counter collections matching the given name"); + report.AppendLine(" Optional. Hides the actual execution time if it is below this threshold"); + report.AppendLine(); + report.AppendLine("Examples:"); + report.AppendLine("pc summary all Shows all events"); + report.AppendLine("pc summary all 5 Shows all events"); + report.AppendLine("pc summary Display.Rendering Shows only the 'Display.Rendering' collection"); break; + case SubCommand.Trigger: - sb.AppendLine("Usage: pc trigger "); - sb.AppendLine("Usage: pc trigger collection "); - sb.AppendLine("Usage: pc trigger collection "); - sb.AppendLine(); - sb.AppendLine("Manages alert triggers."); - sb.AppendLine(); - sb.AppendLine("Arguments:"); - sb.AppendLine(" Optional. Specifies if a specific source or a specific collection should be triggered."); - sb.AppendLine(" - list Lists current triggers"); - sb.AppendLine(" - collection Sets up a trigger for a collection"); - sb.AppendLine(" - clear Clears all trigger entries"); - sb.AppendLine(" - pause Pauses triggering of alerts"); - sb.AppendLine(" - resume Resumes triggering of alerts"); - sb.AppendLine(" - dump Dumps all triggers as commands for copy and paste"); - sb.AppendLine(" Defaults to 'list' if not specified."); - sb.AppendLine(); - sb.AppendLine(" Required if the mode 'collection' is specified."); - sb.AppendLine(" Specifies the name of the collection to be triggered. Must be an exact match."); - sb.AppendLine(); - sb.AppendLine(" Optional. Specifies the name of a specific source. Must be an exact match."); - sb.AppendLine(); - sb.AppendLine(" Required if the mode 'collection' is specified."); - sb.AppendLine(" Specifies the threshold in milliseconds (fractions allowed)."); - sb.AppendLine(" Specify '0' to remove the threshold."); - sb.AppendLine(); - sb.AppendLine("Examples:"); - sb.AppendLine(); - sb.AppendLine("pc trigger collection Display.Rendering 10"); - sb.AppendLine(" Sets up an alert trigger which writes on the console if the execution time of all performance counters in"); - sb.AppendLine(" the 'Display.Rendering' collection exceed 10 milliseconds."); - sb.AppendLine(); - sb.AppendLine("pc trigger collection Display.Rendering 5 Pathoschild.ChestsAnywhere"); - sb.AppendLine(" Sets up an alert trigger to write on the console if the execution time of Pathoschild.ChestsAnywhere in"); - sb.AppendLine(" the 'Display.Rendering' collection exceed 5 milliseconds."); - sb.AppendLine(); - sb.AppendLine("pc trigger collection Display.Rendering 0"); - sb.AppendLine(" Removes the threshold previously defined from the collection. Note that source-specific thresholds are left intact."); - sb.AppendLine(); - sb.AppendLine("pc trigger clear"); - sb.AppendLine(" Clears all previously setup alert triggers."); + report.AppendLine("Usage: pc trigger "); + report.AppendLine("Usage: pc trigger collection "); + report.AppendLine("Usage: pc trigger collection "); + report.AppendLine(); + report.AppendLine("Manages alert triggers."); + report.AppendLine(); + report.AppendLine("Arguments:"); + report.AppendLine(" Optional. Specifies if a specific source or a specific collection should be triggered."); + report.AppendLine(" - list Lists current triggers"); + report.AppendLine(" - collection Sets up a trigger for a collection"); + report.AppendLine(" - clear Clears all trigger entries"); + report.AppendLine(" - pause Pauses triggering of alerts"); + report.AppendLine(" - resume Resumes triggering of alerts"); + report.AppendLine(" - dump Dumps all triggers as commands for copy and paste"); + report.AppendLine(" Defaults to 'list' if not specified."); + report.AppendLine(); + report.AppendLine(" Required if the mode 'collection' is specified."); + report.AppendLine(" Specifies the name of the collection to be triggered. Must be an exact match."); + report.AppendLine(); + report.AppendLine(" Optional. Specifies the name of a specific source. Must be an exact match."); + report.AppendLine(); + report.AppendLine(" Required if the mode 'collection' is specified."); + report.AppendLine(" Specifies the threshold in milliseconds (fractions allowed)."); + report.AppendLine(" Specify '0' to remove the threshold."); + report.AppendLine(); + report.AppendLine("Examples:"); + report.AppendLine(); + report.AppendLine("pc trigger collection Display.Rendering 10"); + report.AppendLine(" Sets up an alert trigger which writes on the console if the execution time of all performance counters in"); + report.AppendLine(" the 'Display.Rendering' collection exceed 10 milliseconds."); + report.AppendLine(); + report.AppendLine("pc trigger collection Display.Rendering 5 Pathoschild.ChestsAnywhere"); + report.AppendLine(" Sets up an alert trigger to write on the console if the execution time of Pathoschild.ChestsAnywhere in"); + report.AppendLine(" the 'Display.Rendering' collection exceed 5 milliseconds."); + report.AppendLine(); + report.AppendLine("pc trigger collection Display.Rendering 0"); + report.AppendLine(" Removes the threshold previously defined from the collection. Note that source-specific thresholds are left intact."); + report.AppendLine(); + report.AppendLine("pc trigger clear"); + report.AppendLine(" Clears all previously setup alert triggers."); break; + case SubCommand.Reset: - sb.AppendLine("Usage: pc reset "); - sb.AppendLine(); - sb.AppendLine("Resets performance counters."); - sb.AppendLine(); - sb.AppendLine("Arguments:"); - sb.AppendLine(" Optional. Specifies if a collection or source should be reset."); - sb.AppendLine(" If omitted, all performance counters are reset."); - sb.AppendLine(); - sb.AppendLine(" - source Clears performance counters for a specific source"); - sb.AppendLine(" - collection Clears performance counters for a specific collection"); - sb.AppendLine(); - sb.AppendLine(" Required if a is given. Specifies the name of either the collection"); - sb.AppendLine(" or the source. The name must be an exact match."); - sb.AppendLine(); - sb.AppendLine("Examples:"); - sb.AppendLine("pc reset Resets all performance counters"); - sb.AppendLine("pc reset source Pathoschild.ChestsAnywhere Resets all performance for the source named Pathoschild.ChestsAnywhere"); - sb.AppendLine("pc reset collection Display.Rendering Resets all performance for the collection named Display.Rendering"); + report.AppendLine("Usage: pc reset "); + report.AppendLine(); + report.AppendLine("Resets performance counters."); + report.AppendLine(); + report.AppendLine("Arguments:"); + report.AppendLine(" Optional. Specifies if a collection or source should be reset."); + report.AppendLine(" If omitted, all performance counters are reset."); + report.AppendLine(); + report.AppendLine(" - source Clears performance counters for a specific source"); + report.AppendLine(" - collection Clears performance counters for a specific collection"); + report.AppendLine(); + report.AppendLine(" Required if a is given. Specifies the name of either the collection"); + report.AppendLine(" or the source. The name must be an exact match."); + report.AppendLine(); + report.AppendLine("Examples:"); + report.AppendLine("pc reset Resets all performance counters"); + report.AppendLine("pc reset source Pathoschild.ChestsAnywhere Resets all performance for the source named Pathoschild.ChestsAnywhere"); + report.AppendLine("pc reset collection Display.Rendering Resets all performance for the collection named Display.Rendering"); break; } - sb.AppendLine(); - monitor.Log(sb.ToString(), LogLevel.Info); + report.AppendLine(); + monitor.Log(report.ToString(), LogLevel.Info); } /// Get the command description. private static string GetDescription() { - StringBuilder sb = new StringBuilder(); - - sb.AppendLine("Displays and configures performance counters."); - sb.AppendLine(); - sb.AppendLine("A performance counter records the invocation time of in-game events being"); - sb.AppendLine("processed by mods or the game itself. See 'concepts' for a detailed explanation."); - sb.AppendLine(); - sb.AppendLine("Usage: pc "); - sb.AppendLine(); - sb.AppendLine("Commands:"); - sb.AppendLine(); - sb.AppendLine(" summary|sum|s Displays a summary of important or all collections"); - sb.AppendLine(" detail|d Shows performance counter information for a given collection"); - sb.AppendLine(" reset|r Resets the performance counters"); - sb.AppendLine(" trigger Configures alert triggers"); - sb.AppendLine(" enable Enables performance counter recording"); - sb.AppendLine(" disable Disables performance counter recording"); - sb.AppendLine(" examples Displays various examples"); - sb.AppendLine(" concepts Displays an explanation of the performance counter concepts"); - sb.AppendLine(" help Displays verbose help for the available commands"); - sb.AppendLine(); - sb.AppendLine("To get help for a specific command, use 'pc help ', for example:"); - sb.AppendLine("pc help summary"); - sb.AppendLine(); - sb.AppendLine("Defaults to summary if no command is given."); - sb.AppendLine(); - - return sb.ToString(); + StringBuilder report = new StringBuilder(); + + report.AppendLine("Displays or configures performance monitoring for diagnose issues."); + report.AppendLine(); + report.AppendLine("A 'performance counter' is a metric which measures execution time across a range of time for a source (e.g. a mod)."); + report.AppendLine("A set of performance counters is organized in a collection to group various areas."); + report.AppendLine("For example, the performance counter collection named 'Display.Rendered' contains one performance"); + report.AppendLine("counter when the game executes the 'Display.Rendered' event, and another counter for each mod which handles it."); + report.AppendLine(); + report.AppendLine("Usage: pc "); + report.AppendLine(); + report.AppendLine("Commands:"); + report.AppendLine(); + report.AppendLine(" summary Show a summary of collections."); + report.AppendLine(" detail Show a summary for a given collection."); + report.AppendLine(" reset Reset all performance counters."); + report.AppendLine(" trigger Configure alert triggers."); + report.AppendLine(" enable Enable performance counter recording."); + report.AppendLine(" disable Disable performance counter recording."); + report.AppendLine(" help Show verbose help for the available commands."); + report.AppendLine(); + report.AppendLine("To get help for a specific command, use 'pc help ', for example:"); + report.AppendLine("pc help summary"); + report.AppendLine(); + report.AppendLine("Defaults to summary if no command is given."); + report.AppendLine(); + + return report.ToString(); + } + + + /********* + ** Private models + *********/ + /// An alert trigger for a collection. + private class CollectionTrigger + { + /********* + ** Accessors + *********/ + /// The collection name. + public string CollectionName { get; } + + /// The trigger threshold. + public double Threshold { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The collection name. + /// The trigger threshold. + public CollectionTrigger(string collectionName, double threshold) + { + this.CollectionName = collectionName; + this.Threshold = threshold; + } + } + + /// An alert triggered for a source. + private class SourceTrigger : CollectionTrigger + { + /********* + ** Accessors + *********/ + /// The source name. + public string SourceName { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The collection name. + /// The source name. + /// The trigger threshold. + public SourceTrigger(string collectionName, string sourceName, double threshold) + : base(collectionName, threshold) + { + this.SourceName = sourceName; + } } } } diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/TrainerCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/TrainerCommand.cs index 8f0d89ba..2b562a08 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/TrainerCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/TrainerCommand.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; @@ -66,7 +66,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands /// The data to display. /// The table header. /// Returns a set of fields for a data value. - /// True to right-align the data, false for left-align. Default false. + /// Whether to right-align the data. protected string GetTableString(IEnumerable data, string[] header, Func getRow, bool rightAlign = false) { // get table data @@ -93,19 +93,11 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands }; lines.AddRange(rows); - if (rightAlign) - { - return string.Join( - Environment.NewLine, - lines.Select(line => string.Join(" | ", line.Select((field, i) => field.PadLeft(widths[i], ' ')).ToArray()) - ) - ); - } - return string.Join( Environment.NewLine, - lines.Select(line => string.Join(" | ", line.Select((field, i) => field.PadRight(widths[i], ' ')).ToArray()) - ) + lines.Select(line => string.Join(" | ", + line.Select((field, i) => rightAlign ? field.PadRight(widths[i], ' ') : field.PadLeft(widths[i], ' ')) + )) ); } } diff --git a/src/SMAPI.Mods.ConsoleCommands/SMAPI.Mods.ConsoleCommands.csproj b/src/SMAPI.Mods.ConsoleCommands/SMAPI.Mods.ConsoleCommands.csproj index f073ac21..ce35bf73 100644 --- a/src/SMAPI.Mods.ConsoleCommands/SMAPI.Mods.ConsoleCommands.csproj +++ b/src/SMAPI.Mods.ConsoleCommands/SMAPI.Mods.ConsoleCommands.csproj @@ -67,10 +67,6 @@ - - - - diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index 76cb6f89..67c7b576 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -55,7 +55,7 @@ namespace StardewModdingAPI /// The URL of the SMAPI home page. internal const string HomePageUrl = "https://smapi.io"; - /// The URL of the SMAPI home page. + /// The default performance counter name for unknown event handlers. internal const string GamePerformanceCounterName = ""; /// The absolute path to the folder containing SMAPI's internal files. @@ -103,6 +103,7 @@ namespace StardewModdingAPI /// The language code for non-translated mod assets. internal static LocalizedContentManager.LanguageCode DefaultLanguage { get; } = LocalizedContentManager.LanguageCode.en; + /********* ** Internal methods *********/ diff --git a/src/SMAPI/Framework/Events/EventManager.cs b/src/SMAPI/Framework/Events/EventManager.cs index 19a4dff8..50dcc9ef 100644 --- a/src/SMAPI/Framework/Events/EventManager.cs +++ b/src/SMAPI/Framework/Events/EventManager.cs @@ -1,4 +1,6 @@ +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Reflection; using StardewModdingAPI.Events; using StardewModdingAPI.Framework.PerformanceCounter; @@ -174,29 +176,32 @@ namespace StardewModdingAPI.Framework.Events /// Construct an instance. /// Writes messages to the log. /// The mod registry with which to identify mods. - /// The performance counter manager. - public EventManager(IMonitor monitor, ModRegistry modRegistry, PerformanceCounterManager performanceCounterManager) + /// Tracks performance metrics. + public EventManager(IMonitor monitor, ModRegistry modRegistry, PerformanceMonitor performanceMonitor) { // create shortcut initializers - ManagedEvent ManageEventOf(string typeName, string eventName) => new ManagedEvent($"{typeName}.{eventName}", monitor, modRegistry, performanceCounterManager); + ManagedEvent ManageEventOf(string typeName, string eventName, bool isPerformanceCritical = false) + { + return new ManagedEvent($"{typeName}.{eventName}", monitor, modRegistry, performanceMonitor, isPerformanceCritical); + } // init events (new) this.MenuChanged = ManageEventOf(nameof(IModEvents.Display), nameof(IDisplayEvents.MenuChanged)); - this.Rendering = ManageEventOf(nameof(IModEvents.Display), nameof(IDisplayEvents.Rendering)); - this.Rendered = ManageEventOf(nameof(IModEvents.Display), nameof(IDisplayEvents.Rendered)); - this.RenderingWorld = ManageEventOf(nameof(IModEvents.Display), nameof(IDisplayEvents.RenderingWorld)); - this.RenderedWorld = ManageEventOf(nameof(IModEvents.Display), nameof(IDisplayEvents.RenderedWorld)); - this.RenderingActiveMenu = ManageEventOf(nameof(IModEvents.Display), nameof(IDisplayEvents.RenderingActiveMenu)); - this.RenderedActiveMenu = ManageEventOf(nameof(IModEvents.Display), nameof(IDisplayEvents.RenderedActiveMenu)); - this.RenderingHud = ManageEventOf(nameof(IModEvents.Display), nameof(IDisplayEvents.RenderingHud)); - this.RenderedHud = ManageEventOf(nameof(IModEvents.Display), nameof(IDisplayEvents.RenderedHud)); + this.Rendering = ManageEventOf(nameof(IModEvents.Display), nameof(IDisplayEvents.Rendering), isPerformanceCritical: true); + this.Rendered = ManageEventOf(nameof(IModEvents.Display), nameof(IDisplayEvents.Rendered), isPerformanceCritical: true); + this.RenderingWorld = ManageEventOf(nameof(IModEvents.Display), nameof(IDisplayEvents.RenderingWorld), isPerformanceCritical: true); + this.RenderedWorld = ManageEventOf(nameof(IModEvents.Display), nameof(IDisplayEvents.RenderedWorld), isPerformanceCritical: true); + this.RenderingActiveMenu = ManageEventOf(nameof(IModEvents.Display), nameof(IDisplayEvents.RenderingActiveMenu), isPerformanceCritical: true); + this.RenderedActiveMenu = ManageEventOf(nameof(IModEvents.Display), nameof(IDisplayEvents.RenderedActiveMenu), isPerformanceCritical: true); + this.RenderingHud = ManageEventOf(nameof(IModEvents.Display), nameof(IDisplayEvents.RenderingHud), isPerformanceCritical: true); + this.RenderedHud = ManageEventOf(nameof(IModEvents.Display), nameof(IDisplayEvents.RenderedHud), isPerformanceCritical: true); this.WindowResized = ManageEventOf(nameof(IModEvents.Display), nameof(IDisplayEvents.WindowResized)); this.GameLaunched = ManageEventOf(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.GameLaunched)); - this.UpdateTicking = ManageEventOf(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.UpdateTicking)); - this.UpdateTicked = ManageEventOf(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.UpdateTicked)); - this.OneSecondUpdateTicking = ManageEventOf(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.OneSecondUpdateTicking)); - this.OneSecondUpdateTicked = ManageEventOf(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.OneSecondUpdateTicked)); + this.UpdateTicking = ManageEventOf(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.UpdateTicking), isPerformanceCritical: true); + this.UpdateTicked = ManageEventOf(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.UpdateTicked), isPerformanceCritical: true); + this.OneSecondUpdateTicking = ManageEventOf(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.OneSecondUpdateTicking), isPerformanceCritical: true); + this.OneSecondUpdateTicked = ManageEventOf(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.OneSecondUpdateTicked), isPerformanceCritical: true); this.SaveCreating = ManageEventOf(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.SaveCreating)); this.SaveCreated = ManageEventOf(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.SaveCreated)); this.Saving = ManageEventOf(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.Saving)); @@ -209,7 +214,7 @@ namespace StardewModdingAPI.Framework.Events this.ButtonPressed = ManageEventOf(nameof(IModEvents.Input), nameof(IInputEvents.ButtonPressed)); this.ButtonReleased = ManageEventOf(nameof(IModEvents.Input), nameof(IInputEvents.ButtonReleased)); - this.CursorMoved = ManageEventOf(nameof(IModEvents.Input), nameof(IInputEvents.CursorMoved)); + this.CursorMoved = ManageEventOf(nameof(IModEvents.Input), nameof(IInputEvents.CursorMoved), isPerformanceCritical: true); this.MouseWheelScrolled = ManageEventOf(nameof(IModEvents.Input), nameof(IInputEvents.MouseWheelScrolled)); this.PeerContextReceived = ManageEventOf(nameof(IModEvents.Multiplayer), nameof(IMultiplayerEvents.PeerContextReceived)); @@ -230,8 +235,15 @@ namespace StardewModdingAPI.Framework.Events this.TerrainFeatureListChanged = ManageEventOf(nameof(IModEvents.World), nameof(IWorldEvents.TerrainFeatureListChanged)); this.LoadStageChanged = ManageEventOf(nameof(IModEvents.Specialized), nameof(ISpecializedEvents.LoadStageChanged)); - this.UnvalidatedUpdateTicking = ManageEventOf(nameof(IModEvents.Specialized), nameof(ISpecializedEvents.UnvalidatedUpdateTicking)); - this.UnvalidatedUpdateTicked = ManageEventOf(nameof(IModEvents.Specialized), nameof(ISpecializedEvents.UnvalidatedUpdateTicked)); + this.UnvalidatedUpdateTicking = ManageEventOf(nameof(IModEvents.Specialized), nameof(ISpecializedEvents.UnvalidatedUpdateTicking), isPerformanceCritical: true); + this.UnvalidatedUpdateTicked = ManageEventOf(nameof(IModEvents.Specialized), nameof(ISpecializedEvents.UnvalidatedUpdateTicked), isPerformanceCritical: true); + } + + /// Get all managed events. + public IEnumerable GetAllEvents() + { + foreach (FieldInfo field in this.GetType().GetFields()) + yield return (IManagedEvent)field.GetValue(this); } } } diff --git a/src/SMAPI/Framework/Events/IManagedEvent.cs b/src/SMAPI/Framework/Events/IManagedEvent.cs index 04476866..e4e3ca08 100644 --- a/src/SMAPI/Framework/Events/IManagedEvent.cs +++ b/src/SMAPI/Framework/Events/IManagedEvent.cs @@ -1,7 +1,15 @@ namespace StardewModdingAPI.Framework.Events { + /// Metadata for an event raised by SMAPI. internal interface IManagedEvent { - string GetName(); + /********* + ** Accessors + *********/ + /// A human-readable name for the event. + string EventName { get; } + + /// Whether the event is typically called at least once per second. + bool IsPerformanceCritical { get; } } } diff --git a/src/SMAPI/Framework/Events/ManagedEvent.cs b/src/SMAPI/Framework/Events/ManagedEvent.cs index dfdd7449..60e5c599 100644 --- a/src/SMAPI/Framework/Events/ManagedEvent.cs +++ b/src/SMAPI/Framework/Events/ManagedEvent.cs @@ -1,13 +1,13 @@ using System; using System.Collections.Generic; using System.Linq; -using PerformanceCounterManager = StardewModdingAPI.Framework.PerformanceCounter.PerformanceCounterManager; +using StardewModdingAPI.Framework.PerformanceCounter; namespace StardewModdingAPI.Framework.Events { /// An event wrapper which intercepts and logs errors in handler code. /// The event arguments type. - internal class ManagedEvent: IManagedEvent + internal class ManagedEvent : IManagedEvent { /********* ** Fields @@ -15,9 +15,6 @@ namespace StardewModdingAPI.Framework.Events /// The underlying event. private event EventHandler Event; - /// A human-readable name for the event. - private readonly string EventName; - /// Writes messages to the log. private readonly IMonitor Monitor; @@ -30,8 +27,19 @@ namespace StardewModdingAPI.Framework.Events /// The cached invocation list. private EventHandler[] CachedInvocationList; - /// The performance counter manager. - private readonly PerformanceCounterManager PerformanceCounterManager; + /// Tracks performance metrics. + private readonly PerformanceMonitor PerformanceMonitor; + + + /********* + ** Accessors + *********/ + /// A human-readable name for the event. + public string EventName { get; } + + /// Whether the event is typically called at least once per second. + public bool IsPerformanceCritical { get; } + /********* ** Public methods @@ -40,19 +48,15 @@ namespace StardewModdingAPI.Framework.Events /// A human-readable name for the event. /// Writes messages to the log. /// The mod registry with which to identify mods. - /// The performance counter manager - public ManagedEvent(string eventName, IMonitor monitor, ModRegistry modRegistry, PerformanceCounterManager performanceCounterManager) + /// Tracks performance metrics. + /// Whether the event is typically called at least once per second. + public ManagedEvent(string eventName, IMonitor monitor, ModRegistry modRegistry, PerformanceMonitor performanceMonitor, bool isPerformanceCritical = false) { this.EventName = eventName; this.Monitor = monitor; this.ModRegistry = modRegistry; - this.PerformanceCounterManager = performanceCounterManager; - } - - /// Gets the event name. - public string GetName() - { - return this.EventName; + this.PerformanceMonitor = performanceMonitor; + this.IsPerformanceCritical = isPerformanceCritical; } /// Get whether anything is listening to the event. @@ -93,22 +97,20 @@ namespace StardewModdingAPI.Framework.Events return; - this.PerformanceCounterManager.BeginTrackInvocation(this.EventName); - - foreach (EventHandler handler in this.CachedInvocationList) + this.PerformanceMonitor.Track(this.EventName, () => { - try - { - this.PerformanceCounterManager.Track(this.EventName, this.GetModNameForPerformanceCounters(handler), - () => handler.Invoke(null, args)); - } - catch (Exception ex) + foreach (EventHandler handler in this.CachedInvocationList) { - this.LogError(handler, ex); + try + { + this.PerformanceMonitor.Track(this.EventName, this.GetModNameForPerformanceCounters(handler), () => handler.Invoke(null, args)); + } + catch (Exception ex) + { + this.LogError(handler, ex); + } } - } - - this.PerformanceCounterManager.EndTrackInvocation(this.EventName); + }); } /// Raise the event and notify all handlers. @@ -139,18 +141,17 @@ namespace StardewModdingAPI.Framework.Events /********* ** Private methods *********/ - + /// Get the mod name for a given event handler to display in performance monitoring reports. + /// The event handler. private string GetModNameForPerformanceCounters(EventHandler handler) { IModMetadata mod = this.GetSourceMod(handler); - if (mod == null) - { return Constants.GamePerformanceCounterName; - } - - return mod.HasManifest() ? mod.Manifest.UniqueID : mod.DisplayName; + return mod.HasManifest() + ? mod.Manifest.UniqueID + : mod.DisplayName; } /// Track an event handler. diff --git a/src/SMAPI/Framework/PerformanceCounter/AlertContext.cs b/src/SMAPI/Framework/PerformanceCounter/AlertContext.cs index 63f0a5ed..76c472e1 100644 --- a/src/SMAPI/Framework/PerformanceCounter/AlertContext.cs +++ b/src/SMAPI/Framework/PerformanceCounter/AlertContext.cs @@ -3,13 +3,20 @@ namespace StardewModdingAPI.Framework.PerformanceCounter /// The context for an alert. internal struct AlertContext { + /********* + ** Accessors + *********/ /// The source which triggered the alert. - public readonly string Source; + public string Source { get; } /// The elapsed milliseconds. - public readonly double Elapsed; + public double Elapsed { get; } - /// Creates a new alert context. + + /********* + ** Public methods + *********/ + /// Construct an instance. /// The source which triggered the alert. /// The elapsed milliseconds. public AlertContext(string source, double elapsed) @@ -18,6 +25,7 @@ namespace StardewModdingAPI.Framework.PerformanceCounter this.Elapsed = elapsed; } + /// Get a human-readable text form of this instance. public override string ToString() { return $"{this.Source}: {this.Elapsed:F2}ms"; diff --git a/src/SMAPI/Framework/PerformanceCounter/AlertEntry.cs b/src/SMAPI/Framework/PerformanceCounter/AlertEntry.cs index b87d8642..494f34a9 100644 --- a/src/SMAPI/Framework/PerformanceCounter/AlertEntry.cs +++ b/src/SMAPI/Framework/PerformanceCounter/AlertEntry.cs @@ -1,28 +1,33 @@ -using System.Collections.Generic; - namespace StardewModdingAPI.Framework.PerformanceCounter { /// A single alert entry. internal struct AlertEntry { + /********* + ** Accessors + *********/ /// The collection in which the alert occurred. - public readonly PerformanceCounterCollection Collection; + public PerformanceCounterCollection Collection { get; } /// The actual execution time in milliseconds. - public readonly double ExecutionTimeMilliseconds; + public double ExecutionTimeMilliseconds { get; } + + /// The configured alert threshold in milliseconds. + public double ThresholdMilliseconds { get; } - /// The configured alert threshold. - public readonly double ThresholdMilliseconds; + /// The sources involved in exceeding the threshold. + public AlertContext[] Context { get; } - /// The context list, which records all sources involved in exceeding the threshold. - public readonly List Context; - /// Creates a new alert entry. - /// The source collection in which the alert occurred. + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The collection in which the alert occurred. /// The actual execution time in milliseconds. - /// The configured threshold in milliseconds. - /// A list of AlertContext to record which sources were involved - public AlertEntry(PerformanceCounterCollection collection, double executionTimeMilliseconds, double thresholdMilliseconds, List context) + /// The configured alert threshold in milliseconds. + /// The sources involved in exceeding the threshold. + public AlertEntry(PerformanceCounterCollection collection, double executionTimeMilliseconds, double thresholdMilliseconds, AlertContext[] context) { this.Collection = collection; this.ExecutionTimeMilliseconds = executionTimeMilliseconds; diff --git a/src/SMAPI/Framework/PerformanceCounter/EventPerformanceCounterCollection.cs b/src/SMAPI/Framework/PerformanceCounter/EventPerformanceCounterCollection.cs deleted file mode 100644 index 4690c512..00000000 --- a/src/SMAPI/Framework/PerformanceCounter/EventPerformanceCounterCollection.cs +++ /dev/null @@ -1,16 +0,0 @@ -using StardewModdingAPI.Framework.Events; - -namespace StardewModdingAPI.Framework.PerformanceCounter -{ - /// Represents a performance counter collection specific to game events. - internal class EventPerformanceCounterCollection: PerformanceCounterCollection - { - /// Creates a new event performance counter collection. - /// The performance counter manager. - /// The ManagedEvent. - /// If the event is flagged as important. - public EventPerformanceCounterCollection(PerformanceCounterManager manager, IManagedEvent @event, bool isImportant) : base(manager, @event.GetName(), isImportant) - { - } - } -} diff --git a/src/SMAPI/Framework/PerformanceCounter/PeakEntry.cs b/src/SMAPI/Framework/PerformanceCounter/PeakEntry.cs index 95dc11f4..abb29541 100644 --- a/src/SMAPI/Framework/PerformanceCounter/PeakEntry.cs +++ b/src/SMAPI/Framework/PerformanceCounter/PeakEntry.cs @@ -1,20 +1,31 @@ using System; -using System.Collections.Generic; namespace StardewModdingAPI.Framework.PerformanceCounter { + /// A peak invocation time. internal struct PeakEntry { + /********* + ** Accessors + *********/ /// The actual execution time in milliseconds. - public readonly double ExecutionTimeMilliseconds; + public double ExecutionTimeMilliseconds { get; } - /// The DateTime when the entry occured. - public DateTime EventTime; + /// When the entry occurred. + public DateTime EventTime { get; } - /// The context list, which records all sources involved in exceeding the threshold. - public readonly List Context; + /// The sources involved in exceeding the threshold. + public AlertContext[] Context { get; } - public PeakEntry(double executionTimeMilliseconds, DateTime eventTime, List context) + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The actual execution time in milliseconds. + /// When the entry occurred. + /// The sources involved in exceeding the threshold. + public PeakEntry(double executionTimeMilliseconds, DateTime eventTime, AlertContext[] context) { this.ExecutionTimeMilliseconds = executionTimeMilliseconds; this.EventTime = eventTime; diff --git a/src/SMAPI/Framework/PerformanceCounter/PerformanceCounter.cs b/src/SMAPI/Framework/PerformanceCounter/PerformanceCounter.cs index e9dfcb14..0fb6482d 100644 --- a/src/SMAPI/Framework/PerformanceCounter/PerformanceCounter.cs +++ b/src/SMAPI/Framework/PerformanceCounter/PerformanceCounter.cs @@ -1,23 +1,32 @@ using System; +using System.Collections.Generic; using System.Linq; -using Cyotek.Collections.Generic; +using Harmony; namespace StardewModdingAPI.Framework.PerformanceCounter { + /// Tracks metadata about a particular code event. internal class PerformanceCounter { + /********* + ** Fields + *********/ /// The size of the ring buffer. - private const int MAX_ENTRIES = 16384; + private readonly int MaxEntries = 16384; /// The collection to which this performance counter belongs. private readonly PerformanceCounterCollection ParentCollection; - /// The circular buffer which stores all performance counter entries - private readonly CircularBuffer _counter; + /// The performance counter entries. + private readonly Stack Entries; - /// The peak execution time + /// The entry with the highest execution time. private PerformanceCounterEntry? PeakPerformanceCounterEntry; + + /********* + ** Accessors + *********/ /// The name of the source. public string Source { get; } @@ -27,118 +36,90 @@ namespace StardewModdingAPI.Framework.PerformanceCounter /// If alerting is enabled or not public bool EnableAlerts { get; set; } + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The collection to which this performance counter belongs. + /// The name of the source. public PerformanceCounter(PerformanceCounterCollection parentCollection, string source) { this.ParentCollection = parentCollection; this.Source = source; - this._counter = new CircularBuffer(PerformanceCounter.MAX_ENTRIES); + this.Entries = new Stack(this.MaxEntries); } - /// Adds a new performance counter entry to the list. Updates the peak entry and adds an alert if - /// monitoring is enabled and the execution time exceeds the threshold. + /// Add a performance counter entry to the list, update monitoring, and raise alerts if needed. /// The entry to add. public void Add(PerformanceCounterEntry entry) { - this._counter.Put(entry); - - if (this.EnableAlerts && entry.ElapsedMilliseconds > this.AlertThresholdMilliseconds) - this.ParentCollection.AddAlert(entry.ElapsedMilliseconds, this.AlertThresholdMilliseconds, - new AlertContext(this.Source, entry.ElapsedMilliseconds)); + // add entry + if (this.Entries.Count > this.MaxEntries) + this.Entries.Pop(); + this.Entries.Add(entry); - if (this.PeakPerformanceCounterEntry == null) + // update metrics + if (this.PeakPerformanceCounterEntry == null || entry.ElapsedMilliseconds > this.PeakPerformanceCounterEntry.Value.ElapsedMilliseconds) this.PeakPerformanceCounterEntry = entry; - else - { - if (entry.ElapsedMilliseconds > this.PeakPerformanceCounterEntry.Value.ElapsedMilliseconds) - this.PeakPerformanceCounterEntry = entry; - } + + // raise alert + if (this.EnableAlerts && entry.ElapsedMilliseconds > this.AlertThresholdMilliseconds) + this.ParentCollection.AddAlert(entry.ElapsedMilliseconds, this.AlertThresholdMilliseconds, new AlertContext(this.Source, entry.ElapsedMilliseconds)); } - /// Clears all performance counter entries and resets the peak entry. + /// Clear all performance counter entries and monitoring. public void Reset() { - this._counter.Clear(); + this.Entries.Clear(); this.PeakPerformanceCounterEntry = null; } - /// Returns the peak entry. - /// The peak entry. + /// Get the peak entry. public PerformanceCounterEntry? GetPeak() { return this.PeakPerformanceCounterEntry; } - /// Returns the peak entry. - /// The peak entry. - public PerformanceCounterEntry? GetPeak(TimeSpan range, DateTime? relativeTo = null) + /// Get the entry with the highest execution time. + /// The time range to search. + /// The end time for the , or null for the current time. + public PerformanceCounterEntry? GetPeak(TimeSpan range, DateTime? endTime = null) { - if (this._counter.IsEmpty) - return null; - - if (relativeTo == null) - relativeTo = DateTime.UtcNow; - - DateTime start = relativeTo.Value.Subtract(range); - - var entries = this._counter.Where(x => (x.EventTime >= start) && (x.EventTime <= relativeTo)).ToList(); - - if (!entries.Any()) - return null; + endTime ??= DateTime.UtcNow; + DateTime startTime = endTime.Value.Subtract(range); - return entries.OrderByDescending(x => x.ElapsedMilliseconds).First(); + return this.Entries + .Where(entry => entry.EventTime >= startTime && entry.EventTime <= endTime) + .OrderByDescending(x => x.ElapsedMilliseconds) + .FirstOrDefault(); } - /// Resets the peak entry. - public void ResetPeak() - { - this.PeakPerformanceCounterEntry = null; - } - - /// Returns the last entry added to the list. - /// The last entry + /// Get the last entry added to the list. public PerformanceCounterEntry? GetLastEntry() { - if (this._counter.IsEmpty) + if (this.Entries.Count == 0) return null; - return this._counter.PeekLast(); + return this.Entries.Peek(); } - /// Returns the average execution time of all entries. - /// The average execution time in milliseconds. - public double GetAverage() + /// Get the average over a given time span. + /// The time range to search. + /// The end time for the , or null for the current time. + public double GetAverage(TimeSpan range, DateTime? endTime = null) { - if (this._counter.IsEmpty) - return 0; - - return this._counter.Average(p => p.ElapsedMilliseconds); - } - - /// Returns the average over a given time span. - /// The time range to retrieve. - /// The DateTime from which to start the average. Defaults to DateTime.UtcNow if null - /// The average execution time in milliseconds. - /// - /// The relativeTo parameter specifies from which point in time the range is subtracted. Example: - /// If DateTime is set to 60 seconds ago, and the range is set to 60 seconds, the method would return - /// the average between all entries between 120s ago and 60s ago. - /// - public double GetAverage(TimeSpan range, DateTime? relativeTo = null) - { - if (this._counter.IsEmpty) - return 0; - - if (relativeTo == null) - relativeTo = DateTime.UtcNow; - - DateTime start = relativeTo.Value.Subtract(range); - - var entries = this._counter.Where(x => (x.EventTime >= start) && (x.EventTime <= relativeTo)).ToList(); + endTime ??= DateTime.UtcNow; + DateTime startTime = endTime.Value.Subtract(range); - if (!entries.Any()) - return 0; + double[] entries = this.Entries + .Where(entry => entry.EventTime >= startTime && entry.EventTime <= endTime) + .Select(p => p.ElapsedMilliseconds) + .ToArray(); - return entries.Average(x => x.ElapsedMilliseconds); + return entries.Length > 0 + ? entries.Average() + : 0; } } } diff --git a/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterCollection.cs b/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterCollection.cs index f469eceb..bd13a36e 100644 --- a/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterCollection.cs +++ b/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterCollection.cs @@ -2,153 +2,129 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; -using Cyotek.Collections.Generic; namespace StardewModdingAPI.Framework.PerformanceCounter { internal class PerformanceCounterCollection { - /// The size of the ring buffer. - private const int MAX_ENTRIES = 16384; + /********* + ** Fields + *********/ + /// The number of peak invocations to keep. + private readonly int MaxEntries = 16384; - /// The list of triggered performance counters. + /// The sources involved in exceeding alert thresholds. private readonly List TriggeredPerformanceCounters = new List(); /// The stopwatch used to track the invocation time. private readonly Stopwatch InvocationStopwatch = new Stopwatch(); /// The performance counter manager. - private readonly PerformanceCounterManager PerformanceCounterManager; + private readonly PerformanceMonitor PerformanceMonitor; - /// Holds the time to calculate the average calls per second. + /// The time to calculate average calls per second. private DateTime CallsPerSecondStart = DateTime.UtcNow; - /// The number of invocations of this collection. + /// The number of invocations. private long CallCount; - /// The circular buffer which stores all peak invocations - private readonly CircularBuffer PeakInvocations; + /// The peak invocations. + private readonly Stack PeakInvocations; + + /********* + ** Accessors + *********/ /// The associated performance counters. public IDictionary PerformanceCounters { get; } = new Dictionary(); /// The name of this collection. public string Name { get; } - /// Flag if this collection is important (used for the console summary command). - public bool IsImportant { get; } + /// Whether the source is typically invoked at least once per second. + public bool IsPerformanceCritical { get; } /// The alert threshold in milliseconds. public double AlertThresholdMilliseconds { get; set; } - /// If alerting is enabled or not + /// Whether alerts are enabled. public bool EnableAlerts { get; set; } - public PerformanceCounterCollection(PerformanceCounterManager performanceCounterManager, string name, bool isImportant) + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The performance counter manager. + /// The name of this collection. + /// Whether the source is typically invoked at least once per second. + public PerformanceCounterCollection(PerformanceMonitor performanceMonitor, string name, bool isPerformanceCritical = false) { - this.PeakInvocations = new CircularBuffer(PerformanceCounterCollection.MAX_ENTRIES); + this.PeakInvocations = new Stack(this.MaxEntries); this.Name = name; - this.PerformanceCounterManager = performanceCounterManager; - this.IsImportant = isImportant; + this.PerformanceMonitor = performanceMonitor; + this.IsPerformanceCritical = isPerformanceCritical; } - public PerformanceCounterCollection(PerformanceCounterManager performanceCounterManager, string name) - { - this.PeakInvocations = new CircularBuffer(PerformanceCounterCollection.MAX_ENTRIES); - this.PerformanceCounterManager = performanceCounterManager; - this.Name = name; - } - - /// Tracks a single invocation for a named source. + /// Track a single invocation for a named source. /// The name of the source. /// The entry. public void Track(string source, PerformanceCounterEntry entry) { + // add entry if (!this.PerformanceCounters.ContainsKey(source)) this.PerformanceCounters.Add(source, new PerformanceCounter(this, source)); - this.PerformanceCounters[source].Add(entry); + // raise alert if (this.EnableAlerts) this.TriggeredPerformanceCounters.Add(new AlertContext(source, entry.ElapsedMilliseconds)); } - /// Returns the average execution time for all non-game internal sources. - /// The average execution time in milliseconds - public double GetModsAverageExecutionTime() - { - return this.PerformanceCounters.Where(p => - p.Key != Constants.GamePerformanceCounterName).Sum(p => p.Value.GetAverage()); - } - - /// Returns the average execution time for all non-game internal sources. + /// Get the average execution time for all non-game internal sources in milliseconds. /// The interval for which to get the average, relative to now - /// The average execution time in milliseconds public double GetModsAverageExecutionTime(TimeSpan interval) { - return this.PerformanceCounters.Where(p => - p.Key != Constants.GamePerformanceCounterName).Sum(p => p.Value.GetAverage(interval)); - } - - /// Returns the overall average execution time. - /// The average execution time in milliseconds - public double GetAverageExecutionTime() - { - return this.PerformanceCounters.Sum(p => p.Value.GetAverage()); + return this.PerformanceCounters + .Where(entry => entry.Key != Constants.GamePerformanceCounterName) + .Sum(entry => entry.Value.GetAverage(interval)); } - /// Returns the overall average execution time. + /// Get the overall average execution time in milliseconds. /// The interval for which to get the average, relative to now - /// The average execution time in milliseconds public double GetAverageExecutionTime(TimeSpan interval) { - return this.PerformanceCounters.Sum(p => p.Value.GetAverage(interval)); + return this.PerformanceCounters + .Sum(entry => entry.Value.GetAverage(interval)); } - /// Returns the average execution time for game-internal sources. - /// The average execution time in milliseconds - public double GetGameAverageExecutionTime() - { - if (this.PerformanceCounters.TryGetValue(Constants.GamePerformanceCounterName, out PerformanceCounter gameExecTime)) - return gameExecTime.GetAverage(); - - return 0; - } - - /// Returns the average execution time for game-internal sources. - /// The average execution time in milliseconds + /// Get the average execution time for game-internal sources in milliseconds. public double GetGameAverageExecutionTime(TimeSpan interval) { - if (this.PerformanceCounters.TryGetValue(Constants.GamePerformanceCounterName, out PerformanceCounter gameExecTime)) - return gameExecTime.GetAverage(interval); - - return 0; + return this.PerformanceCounters.TryGetValue(Constants.GamePerformanceCounterName, out PerformanceCounter gameExecTime) + ? gameExecTime.GetAverage(interval) + : 0; } - /// Returns the peak execution time - /// The interval for which to get the peak, relative to - /// The DateTime which the is relative to, or DateTime.Now if not given - /// The peak execution time - public double GetPeakExecutionTime(TimeSpan range, DateTime? relativeTo = null) + /// Get the peak execution time in milliseconds. + /// The time range to search. + /// The end time for the , or null for the current time. + public double GetPeakExecutionTime(TimeSpan range, DateTime? endTime = null) { - if (this.PeakInvocations.IsEmpty) + if (this.PeakInvocations.Count == 0) return 0; - if (relativeTo == null) - relativeTo = DateTime.UtcNow; - - DateTime start = relativeTo.Value.Subtract(range); - - var entries = this.PeakInvocations.Where(x => (x.EventTime >= start) && (x.EventTime <= relativeTo)).ToList(); + endTime ??= DateTime.UtcNow; + DateTime startTime = endTime.Value.Subtract(range); - if (!entries.Any()) - return 0; - - return entries.OrderByDescending(x => x.ExecutionTimeMilliseconds).First().ExecutionTimeMilliseconds; + return this.PeakInvocations + .Where(entry => entry.EventTime >= startTime && entry.EventTime <= endTime) + .OrderByDescending(x => x.ExecutionTimeMilliseconds) + .Select(p => p.ExecutionTimeMilliseconds) + .FirstOrDefault(); } - /// Begins tracking the invocation of this collection. + /// Start tracking the invocation of this collection. public void BeginTrackInvocation() { this.TriggeredPerformanceCounters.Clear(); @@ -158,60 +134,58 @@ namespace StardewModdingAPI.Framework.PerformanceCounter this.CallCount++; } - /// Ends tracking the invocation of this collection. Also records an alert if alerting is enabled - /// and the invocation time exceeds the threshold. + /// End tracking the invocation of this collection, and raise an alert if needed. public void EndTrackInvocation() { this.InvocationStopwatch.Stop(); - this.PeakInvocations.Put( - new PeakEntry(this.InvocationStopwatch.Elapsed.TotalMilliseconds, - DateTime.UtcNow, - this.TriggeredPerformanceCounters)); - - if (!this.EnableAlerts) return; + // add invocation + if (this.PeakInvocations.Count >= this.MaxEntries) + this.PeakInvocations.Pop(); + this.PeakInvocations.Push(new PeakEntry(this.InvocationStopwatch.Elapsed.TotalMilliseconds, DateTime.UtcNow, this.TriggeredPerformanceCounters.ToArray())); - if (this.InvocationStopwatch.Elapsed.TotalMilliseconds >= this.AlertThresholdMilliseconds) - this.AddAlert(this.InvocationStopwatch.Elapsed.TotalMilliseconds, - this.AlertThresholdMilliseconds, this.TriggeredPerformanceCounters); + // raise alert + if (this.EnableAlerts && this.InvocationStopwatch.Elapsed.TotalMilliseconds >= this.AlertThresholdMilliseconds) + this.AddAlert(this.InvocationStopwatch.Elapsed.TotalMilliseconds, this.AlertThresholdMilliseconds, this.TriggeredPerformanceCounters.ToArray()); } - /// Adds an alert. + /// Add an alert. /// The execution time in milliseconds. /// The configured threshold. - /// The list of alert contexts. - public void AddAlert(double executionTimeMilliseconds, double thresholdMilliseconds, List alerts) + /// The sources involved in exceeding the threshold. + public void AddAlert(double executionTimeMilliseconds, double thresholdMilliseconds, AlertContext[] alerts) { - this.PerformanceCounterManager.AddAlert(new AlertEntry(this, executionTimeMilliseconds, - thresholdMilliseconds, alerts)); + this.PerformanceMonitor.AddAlert( + new AlertEntry(this, executionTimeMilliseconds, thresholdMilliseconds, alerts) + ); } - /// Adds an alert for a single AlertContext + /// Add an alert. /// The execution time in milliseconds. /// The configured threshold. - /// The context + /// The source involved in exceeding the threshold. public void AddAlert(double executionTimeMilliseconds, double thresholdMilliseconds, AlertContext alert) { - this.AddAlert(executionTimeMilliseconds, thresholdMilliseconds, new List() {alert}); + this.AddAlert(executionTimeMilliseconds, thresholdMilliseconds, new[] { alert }); } - /// Resets the calls per second counter. + /// Reset the calls per second counter. public void ResetCallsPerSecond() { this.CallCount = 0; this.CallsPerSecondStart = DateTime.UtcNow; } - /// Resets all performance counters in this collection. + /// Reset all performance counters in this collection. public void Reset() { this.PeakInvocations.Clear(); - foreach (var i in this.PerformanceCounters) - i.Value.Reset(); + foreach (var counter in this.PerformanceCounters) + counter.Value.Reset(); } - /// Resets the performance counter for a specific source. - /// The source name + /// Reset the performance counter for a specific source. + /// The source name. public void ResetSource(string source) { foreach (var i in this.PerformanceCounters) @@ -219,15 +193,13 @@ namespace StardewModdingAPI.Framework.PerformanceCounter i.Value.Reset(); } - /// Returns the average calls per second. - /// The average calls per second. + /// Get the average calls per second. public long GetAverageCallsPerSecond() { - long runtimeInSeconds = (long) DateTime.UtcNow.Subtract(this.CallsPerSecondStart).TotalSeconds; - - if (runtimeInSeconds == 0) return 0; - - return this.CallCount / runtimeInSeconds; + long runtimeInSeconds = (long)DateTime.UtcNow.Subtract(this.CallsPerSecondStart).TotalSeconds; + return runtimeInSeconds > 0 + ? this.CallCount / runtimeInSeconds + : 0; } } } diff --git a/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterEntry.cs b/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterEntry.cs index a50fce7d..a1d78fc8 100644 --- a/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterEntry.cs +++ b/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterEntry.cs @@ -2,13 +2,29 @@ using System; namespace StardewModdingAPI.Framework.PerformanceCounter { - /// A single performance counter entry. Records the DateTime of the event and the elapsed millisecond. + /// A single performance counter entry. internal struct PerformanceCounterEntry { - /// The DateTime when the entry occured. - public DateTime EventTime; + /********* + ** Accessors + *********/ + /// When the entry occurred. + public DateTime EventTime { get; } - /// The elapsed milliseconds - public double ElapsedMilliseconds; + /// The elapsed milliseconds. + public double ElapsedMilliseconds { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// When the entry occurred. + /// The elapsed milliseconds. + public PerformanceCounterEntry(DateTime eventTime, double elapsedMilliseconds) + { + this.EventTime = eventTime; + this.ElapsedMilliseconds = elapsedMilliseconds; + } } } diff --git a/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterManager.cs b/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterManager.cs index bd964442..81e4e468 100644 --- a/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterManager.cs +++ b/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterManager.cs @@ -7,12 +7,14 @@ using StardewModdingAPI.Framework.Events; namespace StardewModdingAPI.Framework.PerformanceCounter { - internal class PerformanceCounterManager + /// Tracks performance metrics. + internal class PerformanceMonitor { - public HashSet PerformanceCounterCollections = new HashSet(); - + /********* + ** Fields + *********/ /// The recorded alerts. - private readonly List Alerts = new List(); + private readonly IList Alerts = new List(); /// The monitor for output logging. private readonly IMonitor Monitor; @@ -20,62 +22,64 @@ namespace StardewModdingAPI.Framework.PerformanceCounter /// The invocation stopwatch. private readonly Stopwatch InvocationStopwatch = new Stopwatch(); - /// Specifies if alerts should be paused. + /// The underlying performance counter collections. + private readonly IDictionary Collections = new Dictionary(StringComparer.InvariantCultureIgnoreCase); + + + /********* + ** Accessors + *********/ + /// Whether alerts are paused. public bool PauseAlerts { get; set; } - /// Specifies if performance counter tracking should be enabled. + /// Whether performance counter tracking is enabled. public bool EnableTracking { get; set; } - /// Constructs a performance counter manager. + + /********* + ** Public methods + *********/ + /// Construct an instance. /// The monitor for output logging. - public PerformanceCounterManager(IMonitor monitor) + public PerformanceMonitor(IMonitor monitor) { this.Monitor = monitor; } - /// Resets all performance counters in all collections. + /// Reset all performance counters in all collections. public void Reset() { - foreach (PerformanceCounterCollection collection in this.PerformanceCounterCollections) - { + foreach (PerformanceCounterCollection collection in this.Collections.Values) collection.Reset(); - } - - foreach (var eventPerformanceCounter in - this.PerformanceCounterCollections.SelectMany(performanceCounter => performanceCounter.PerformanceCounters)) - { - eventPerformanceCounter.Value.Reset(); - } } - /// Begins tracking the invocation for a collection. - /// The collection name - public void BeginTrackInvocation(string collectionName) + /// Track the invocation time for a collection. + /// The name of the collection. + /// The action to execute and track. + public void Track(string collectionName, Action action) { if (!this.EnableTracking) { + action(); return; } - this.GetOrCreateCollectionByName(collectionName).BeginTrackInvocation(); - } - - /// Ends tracking the invocation for a collection. - /// - public void EndTrackInvocation(string collectionName) - { - if (!this.EnableTracking) + PerformanceCounterCollection collection = this.GetOrCreateCollectionByName(collectionName); + collection.BeginTrackInvocation(); + try { - return; + action(); + } + finally + { + collection.EndTrackInvocation(); } - - this.GetOrCreateCollectionByName(collectionName).EndTrackInvocation(); } - /// Tracks a single performance counter invocation in a specific collection. + /// Track a single performance counter invocation in a specific collection. /// The name of the collection. /// The name of the source. - /// The action to execute and track invocation time for. + /// The action to execute and track. public void Track(string collectionName, string sourceName, Action action) { if (!this.EnableTracking) @@ -84,6 +88,7 @@ namespace StardewModdingAPI.Framework.PerformanceCounter return; } + PerformanceCounterCollection collection = this.GetOrCreateCollectionByName(collectionName); DateTime eventTime = DateTime.UtcNow; this.InvocationStopwatch.Reset(); this.InvocationStopwatch.Start(); @@ -95,79 +100,50 @@ namespace StardewModdingAPI.Framework.PerformanceCounter finally { this.InvocationStopwatch.Stop(); - - this.GetOrCreateCollectionByName(collectionName).Track(sourceName, new PerformanceCounterEntry - { - EventTime = eventTime, - ElapsedMilliseconds = this.InvocationStopwatch.Elapsed.TotalMilliseconds - }); + collection.Track(sourceName, new PerformanceCounterEntry(eventTime, this.InvocationStopwatch.Elapsed.TotalMilliseconds)); } } - /// Gets a collection by name. - /// The name of the collection. - /// The collection or null if none was found. - private PerformanceCounterCollection GetCollectionByName(string name) - { - return this.PerformanceCounterCollections.FirstOrDefault(collection => collection.Name == name); - } - - /// Gets a collection by name and creates it if it doesn't exist. - /// The name of the collection. - /// The collection. - private PerformanceCounterCollection GetOrCreateCollectionByName(string name) - { - PerformanceCounterCollection collection = this.GetCollectionByName(name); - - if (collection != null) return collection; - - collection = new PerformanceCounterCollection(this, name); - this.PerformanceCounterCollections.Add(collection); - - return collection; - } - - /// Resets the performance counters for a specific collection. + /// Reset the performance counters for a specific collection. /// The collection name. public void ResetCollection(string name) { - foreach (PerformanceCounterCollection performanceCounterCollection in - this.PerformanceCounterCollections.Where(performanceCounterCollection => - performanceCounterCollection.Name.Equals(name, StringComparison.InvariantCultureIgnoreCase))) + if (this.Collections.TryGetValue(name, out PerformanceCounterCollection collection)) { - performanceCounterCollection.ResetCallsPerSecond(); - performanceCounterCollection.Reset(); + collection.ResetCallsPerSecond(); + collection.Reset(); } } - /// Resets performance counters for a specific source. + /// Reset performance counters for a specific source. /// The name of the source. public void ResetSource(string name) { - foreach (PerformanceCounterCollection performanceCounterCollection in this.PerformanceCounterCollections) + foreach (PerformanceCounterCollection performanceCounterCollection in this.Collections.Values) performanceCounterCollection.ResetSource(name); } /// Print any queued alerts. public void PrintQueuedAlerts() { - if (this.Alerts.Count == 0) return; + if (this.Alerts.Count == 0) + return; - StringBuilder sb = new StringBuilder(); + StringBuilder report = new StringBuilder(); foreach (AlertEntry alert in this.Alerts) { - sb.AppendLine($"{alert.Collection.Name} took {alert.ExecutionTimeMilliseconds:F2}ms (exceeded threshold of {alert.ThresholdMilliseconds:F2}ms)"); + report.AppendLine($"{alert.Collection.Name} took {alert.ExecutionTimeMilliseconds:F2}ms (exceeded threshold of {alert.ThresholdMilliseconds:F2}ms)"); foreach (AlertContext context in alert.Context.OrderByDescending(p => p.Elapsed)) - sb.AppendLine(context.ToString()); + report.AppendLine(context.ToString()); } this.Alerts.Clear(); - this.Monitor.Log(sb.ToString(), LogLevel.Error); + this.Monitor.Log(report.ToString(), LogLevel.Error); } - /// Adds an alert to the queue. + /// Add an alert to the queue. /// The alert to add. public void AddAlert(AlertEntry entry) { @@ -175,68 +151,34 @@ namespace StardewModdingAPI.Framework.PerformanceCounter this.Alerts.Add(entry); } - /// Initialized the default performance counter collections. + /// Initialize the default performance counter collections. /// The event manager. public void InitializePerformanceCounterCollections(EventManager eventManager) { - this.PerformanceCounterCollections = new HashSet() + foreach (IManagedEvent @event in eventManager.GetAllEvents()) + this.Collections[@event.EventName] = new PerformanceCounterCollection(this, @event.EventName, @event.IsPerformanceCritical); + } + + /// Get the underlying performance counters. + public IEnumerable GetCollections() + { + return this.Collections.Values; + } + + + /********* + ** Public methods + *********/ + /// Get a collection by name and creates it if it doesn't exist. + /// The name of the collection. + private PerformanceCounterCollection GetOrCreateCollectionByName(string name) + { + if (!this.Collections.TryGetValue(name, out PerformanceCounterCollection collection)) { - new EventPerformanceCounterCollection(this, eventManager.MenuChanged, false), - - // Rendering Events - new EventPerformanceCounterCollection(this, eventManager.Rendering, false), - new EventPerformanceCounterCollection(this, eventManager.Rendered, true), - new EventPerformanceCounterCollection(this, eventManager.RenderingWorld, false), - new EventPerformanceCounterCollection(this, eventManager.RenderedWorld, true), - new EventPerformanceCounterCollection(this, eventManager.RenderingActiveMenu, false), - new EventPerformanceCounterCollection(this, eventManager.RenderedActiveMenu, true), - new EventPerformanceCounterCollection(this, eventManager.RenderingHud, false), - new EventPerformanceCounterCollection(this, eventManager.RenderedHud, true), - - new EventPerformanceCounterCollection(this, eventManager.WindowResized, false), - new EventPerformanceCounterCollection(this, eventManager.GameLaunched, false), - new EventPerformanceCounterCollection(this, eventManager.UpdateTicking, true), - new EventPerformanceCounterCollection(this, eventManager.UpdateTicked, true), - new EventPerformanceCounterCollection(this, eventManager.OneSecondUpdateTicking, true), - new EventPerformanceCounterCollection(this, eventManager.OneSecondUpdateTicked, true), - - new EventPerformanceCounterCollection(this, eventManager.SaveCreating, false), - new EventPerformanceCounterCollection(this, eventManager.SaveCreated, false), - new EventPerformanceCounterCollection(this, eventManager.Saving, false), - new EventPerformanceCounterCollection(this, eventManager.Saved, false), - - new EventPerformanceCounterCollection(this, eventManager.DayStarted, false), - new EventPerformanceCounterCollection(this, eventManager.DayEnding, false), - - new EventPerformanceCounterCollection(this, eventManager.TimeChanged, true), - - new EventPerformanceCounterCollection(this, eventManager.ReturnedToTitle, false), - - new EventPerformanceCounterCollection(this, eventManager.ButtonPressed, true), - new EventPerformanceCounterCollection(this, eventManager.ButtonReleased, true), - new EventPerformanceCounterCollection(this, eventManager.CursorMoved, true), - new EventPerformanceCounterCollection(this, eventManager.MouseWheelScrolled, true), - - new EventPerformanceCounterCollection(this, eventManager.PeerContextReceived, false), - new EventPerformanceCounterCollection(this, eventManager.ModMessageReceived, false), - new EventPerformanceCounterCollection(this, eventManager.PeerDisconnected, false), - new EventPerformanceCounterCollection(this, eventManager.InventoryChanged, true), - new EventPerformanceCounterCollection(this, eventManager.LevelChanged, false), - new EventPerformanceCounterCollection(this, eventManager.Warped, false), - - new EventPerformanceCounterCollection(this, eventManager.LocationListChanged, false), - new EventPerformanceCounterCollection(this, eventManager.BuildingListChanged, false), - new EventPerformanceCounterCollection(this, eventManager.LocationListChanged, false), - new EventPerformanceCounterCollection(this, eventManager.DebrisListChanged, true), - new EventPerformanceCounterCollection(this, eventManager.LargeTerrainFeatureListChanged, true), - new EventPerformanceCounterCollection(this, eventManager.NpcListChanged, false), - new EventPerformanceCounterCollection(this, eventManager.ObjectListChanged, true), - new EventPerformanceCounterCollection(this, eventManager.ChestInventoryChanged, true), - new EventPerformanceCounterCollection(this, eventManager.TerrainFeatureListChanged, true), - new EventPerformanceCounterCollection(this, eventManager.LoadStageChanged, false), - new EventPerformanceCounterCollection(this, eventManager.UnvalidatedUpdateTicking, false), - new EventPerformanceCounterCollection(this, eventManager.UnvalidatedUpdateTicked, false), - }; + collection = new PerformanceCounterCollection(this, name); + this.Collections[name] = collection; + } + return collection; } } } diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index af7513e3..ac89587e 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -23,6 +23,7 @@ using StardewModdingAPI.Framework.Models; using StardewModdingAPI.Framework.ModHelpers; using StardewModdingAPI.Framework.ModLoading; using StardewModdingAPI.Framework.Patching; +using StardewModdingAPI.Framework.PerformanceCounter; using StardewModdingAPI.Framework.Reflection; using StardewModdingAPI.Framework.Serialization; using StardewModdingAPI.Patches; @@ -33,7 +34,6 @@ using StardewModdingAPI.Toolkit.Serialization; using StardewModdingAPI.Toolkit.Utilities; using StardewValley; using Object = StardewValley.Object; -using PerformanceCounterManager = StardewModdingAPI.Framework.PerformanceCounter.PerformanceCounterManager; using ThreadState = System.Threading.ThreadState; namespace StardewModdingAPI.Framework @@ -135,8 +135,8 @@ namespace StardewModdingAPI.Framework internal static DeprecationManager DeprecationManager { get; private set; } /// Manages performance counters. - /// This is initialized after the game starts. This is accessed directly because it's not part of the normal class model. - internal static PerformanceCounterManager PerformanceCounterManager { get; private set; } + /// This is initialized after the game starts. This is non-private for use by Console Commands. + internal static PerformanceMonitor PerformanceMonitor { get; private set; } /********* @@ -167,9 +167,9 @@ namespace StardewModdingAPI.Framework }; this.MonitorForGame = this.GetSecondaryMonitor("game"); - SCore.PerformanceCounterManager = new PerformanceCounterManager(this.Monitor); - this.EventManager = new EventManager(this.Monitor, this.ModRegistry, SCore.PerformanceCounterManager); - SCore.PerformanceCounterManager.InitializePerformanceCounterCollections(this.EventManager); + SCore.PerformanceMonitor = new PerformanceMonitor(this.Monitor); + this.EventManager = new EventManager(this.Monitor, this.ModRegistry, SCore.PerformanceMonitor); + SCore.PerformanceMonitor.InitializePerformanceCounterCollections(this.EventManager); SCore.DeprecationManager = new DeprecationManager(this.Monitor, this.ModRegistry); @@ -248,7 +248,7 @@ namespace StardewModdingAPI.Framework jsonHelper: this.Toolkit.JsonHelper, modRegistry: this.ModRegistry, deprecationManager: SCore.DeprecationManager, - performanceCounterManager: SCore.PerformanceCounterManager, + performanceMonitor: SCore.PerformanceMonitor, onGameInitialized: this.InitializeAfterGameStart, onGameExiting: this.Dispose, cancellationToken: this.CancellationToken, @@ -1307,6 +1307,7 @@ namespace StardewModdingAPI.Framework this.ReloadTranslations(this.ModRegistry.GetAll(contentPacks: false)); this.Monitor.Log("Reloaded translation files for all mods. This only affects new translations the mods fetch; if they cached some text, it may not be updated.", LogLevel.Info); break; + default: throw new NotSupportedException($"Unrecognized core SMAPI command '{name}'."); } diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index 266e2e6f..352859ec 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -59,7 +59,8 @@ namespace StardewModdingAPI.Framework /// Manages deprecation warnings. private readonly DeprecationManager DeprecationManager; - private readonly PerformanceCounterManager PerformanceCounterManager; + /// Tracks performance metrics. + private readonly PerformanceMonitor PerformanceMonitor; /// The maximum number of consecutive attempts SMAPI should make to recover from a draw error. private readonly Countdown DrawCrashTimer = new Countdown(60); // 60 ticks = roughly one second @@ -155,12 +156,12 @@ namespace StardewModdingAPI.Framework /// Encapsulates SMAPI's JSON file parsing. /// Tracks the installed mods. /// Manages deprecation warnings. - /// Manages performance monitoring. + /// Tracks performance metrics. /// A callback to invoke after the game finishes initializing. /// A callback to invoke when the game exits. /// Propagates notification that SMAPI should exit. /// Whether to log network traffic. - internal SGame(Monitor monitor, IMonitor monitorForGame, Reflector reflection, Translator translator, EventManager eventManager, JsonHelper jsonHelper, ModRegistry modRegistry, DeprecationManager deprecationManager, PerformanceCounterManager performanceCounterManager, Action onGameInitialized, Action onGameExiting, CancellationTokenSource cancellationToken, bool logNetworkTraffic) + internal SGame(Monitor monitor, IMonitor monitorForGame, Reflector reflection, Translator translator, EventManager eventManager, JsonHelper jsonHelper, ModRegistry modRegistry, DeprecationManager deprecationManager, PerformanceMonitor performanceMonitor, Action onGameInitialized, Action onGameExiting, CancellationTokenSource cancellationToken, bool logNetworkTraffic) { this.OnLoadingFirstAsset = SGame.ConstructorHack.OnLoadingFirstAsset; SGame.ConstructorHack = null; @@ -180,7 +181,7 @@ namespace StardewModdingAPI.Framework this.Reflection = reflection; this.Translator = translator; this.DeprecationManager = deprecationManager; - this.PerformanceCounterManager = performanceCounterManager; + this.PerformanceMonitor = performanceMonitor; this.OnGameInitialized = onGameInitialized; this.OnGameExiting = onGameExiting; Game1.input = new SInputState(); @@ -312,7 +313,7 @@ namespace StardewModdingAPI.Framework try { this.DeprecationManager.PrintQueued(); - this.PerformanceCounterManager.PrintQueuedAlerts(); + this.PerformanceMonitor.PrintQueuedAlerts(); /********* ** First-tick initialization diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index 933590d3..c26ae29a 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -11,7 +11,7 @@ using StardewModdingAPI.Framework; using StardewModdingAPI.Toolkit.Utilities; [assembly: InternalsVisibleTo("SMAPI.Tests")] -[assembly: InternalsVisibleTo("ConsoleCommands")] +[assembly: InternalsVisibleTo("ConsoleCommands")] // for performance monitoring commands [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] // Moq for unit testing namespace StardewModdingAPI { diff --git a/src/SMAPI/SMAPI.csproj b/src/SMAPI/SMAPI.csproj index 0bc290ac..3bb73295 100644 --- a/src/SMAPI/SMAPI.csproj +++ b/src/SMAPI/SMAPI.csproj @@ -15,12 +15,7 @@ icon.ico - - SMAPI_FOR_WINDOWS - - - -- cgit From 910b4a2c4361c429b09bd35fa52d51b24cc17bc2 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 26 Jan 2020 19:52:31 -0500 Subject: tweak namespace --- .../Commands/Other/PerformanceCounterCommand.cs | 2 +- src/SMAPI/Framework/Events/EventManager.cs | 2 +- src/SMAPI/Framework/Events/ManagedEvent.cs | 2 +- .../Framework/PerformanceCounter/AlertContext.cs | 34 ---- .../Framework/PerformanceCounter/AlertEntry.cs | 38 ---- .../Framework/PerformanceCounter/PeakEntry.cs | 35 ---- .../PerformanceCounter/PerformanceCounter.cs | 125 ------------- .../PerformanceCounterCollection.cs | 205 --------------------- .../PerformanceCounter/PerformanceCounterEntry.cs | 30 --- .../PerformanceCounterManager.cs | 184 ------------------ .../PerformanceMonitoring/AlertContext.cs | 34 ++++ .../Framework/PerformanceMonitoring/AlertEntry.cs | 38 ++++ .../Framework/PerformanceMonitoring/PeakEntry.cs | 35 ++++ .../PerformanceMonitoring/PerformanceCounter.cs | 125 +++++++++++++ .../PerformanceCounterCollection.cs | 205 +++++++++++++++++++++ .../PerformanceCounterEntry.cs | 30 +++ .../PerformanceMonitoring/PerformanceMonitor.cs | 184 ++++++++++++++++++ src/SMAPI/Framework/SCore.cs | 2 +- src/SMAPI/Framework/SGame.cs | 2 +- 19 files changed, 656 insertions(+), 656 deletions(-) delete mode 100644 src/SMAPI/Framework/PerformanceCounter/AlertContext.cs delete mode 100644 src/SMAPI/Framework/PerformanceCounter/AlertEntry.cs delete mode 100644 src/SMAPI/Framework/PerformanceCounter/PeakEntry.cs delete mode 100644 src/SMAPI/Framework/PerformanceCounter/PerformanceCounter.cs delete mode 100644 src/SMAPI/Framework/PerformanceCounter/PerformanceCounterCollection.cs delete mode 100644 src/SMAPI/Framework/PerformanceCounter/PerformanceCounterEntry.cs delete mode 100644 src/SMAPI/Framework/PerformanceCounter/PerformanceCounterManager.cs create mode 100644 src/SMAPI/Framework/PerformanceMonitoring/AlertContext.cs create mode 100644 src/SMAPI/Framework/PerformanceMonitoring/AlertEntry.cs create mode 100644 src/SMAPI/Framework/PerformanceMonitoring/PeakEntry.cs create mode 100644 src/SMAPI/Framework/PerformanceMonitoring/PerformanceCounter.cs create mode 100644 src/SMAPI/Framework/PerformanceMonitoring/PerformanceCounterCollection.cs create mode 100644 src/SMAPI/Framework/PerformanceMonitoring/PerformanceCounterEntry.cs create mode 100644 src/SMAPI/Framework/PerformanceMonitoring/PerformanceMonitor.cs (limited to 'src/SMAPI.Mods.ConsoleCommands/Framework') diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/PerformanceCounterCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/PerformanceCounterCommand.cs index 820f1939..da171a44 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/PerformanceCounterCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/PerformanceCounterCommand.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Text; using StardewModdingAPI.Framework; -using StardewModdingAPI.Framework.PerformanceCounter; +using StardewModdingAPI.Framework.PerformanceMonitoring; namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other { diff --git a/src/SMAPI/Framework/Events/EventManager.cs b/src/SMAPI/Framework/Events/EventManager.cs index 50dcc9ef..a9dfda97 100644 --- a/src/SMAPI/Framework/Events/EventManager.cs +++ b/src/SMAPI/Framework/Events/EventManager.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Reflection; using StardewModdingAPI.Events; -using StardewModdingAPI.Framework.PerformanceCounter; +using StardewModdingAPI.Framework.PerformanceMonitoring; namespace StardewModdingAPI.Framework.Events { diff --git a/src/SMAPI/Framework/Events/ManagedEvent.cs b/src/SMAPI/Framework/Events/ManagedEvent.cs index 60e5c599..118b73ac 100644 --- a/src/SMAPI/Framework/Events/ManagedEvent.cs +++ b/src/SMAPI/Framework/Events/ManagedEvent.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; -using StardewModdingAPI.Framework.PerformanceCounter; +using StardewModdingAPI.Framework.PerformanceMonitoring; namespace StardewModdingAPI.Framework.Events { diff --git a/src/SMAPI/Framework/PerformanceCounter/AlertContext.cs b/src/SMAPI/Framework/PerformanceCounter/AlertContext.cs deleted file mode 100644 index 76c472e1..00000000 --- a/src/SMAPI/Framework/PerformanceCounter/AlertContext.cs +++ /dev/null @@ -1,34 +0,0 @@ -namespace StardewModdingAPI.Framework.PerformanceCounter -{ - /// The context for an alert. - internal struct AlertContext - { - /********* - ** Accessors - *********/ - /// The source which triggered the alert. - public string Source { get; } - - /// The elapsed milliseconds. - public double Elapsed { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The source which triggered the alert. - /// The elapsed milliseconds. - public AlertContext(string source, double elapsed) - { - this.Source = source; - this.Elapsed = elapsed; - } - - /// Get a human-readable text form of this instance. - public override string ToString() - { - return $"{this.Source}: {this.Elapsed:F2}ms"; - } - } -} diff --git a/src/SMAPI/Framework/PerformanceCounter/AlertEntry.cs b/src/SMAPI/Framework/PerformanceCounter/AlertEntry.cs deleted file mode 100644 index 494f34a9..00000000 --- a/src/SMAPI/Framework/PerformanceCounter/AlertEntry.cs +++ /dev/null @@ -1,38 +0,0 @@ -namespace StardewModdingAPI.Framework.PerformanceCounter -{ - /// A single alert entry. - internal struct AlertEntry - { - /********* - ** Accessors - *********/ - /// The collection in which the alert occurred. - public PerformanceCounterCollection Collection { get; } - - /// The actual execution time in milliseconds. - public double ExecutionTimeMilliseconds { get; } - - /// The configured alert threshold in milliseconds. - public double ThresholdMilliseconds { get; } - - /// The sources involved in exceeding the threshold. - public AlertContext[] Context { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The collection in which the alert occurred. - /// The actual execution time in milliseconds. - /// The configured alert threshold in milliseconds. - /// The sources involved in exceeding the threshold. - public AlertEntry(PerformanceCounterCollection collection, double executionTimeMilliseconds, double thresholdMilliseconds, AlertContext[] context) - { - this.Collection = collection; - this.ExecutionTimeMilliseconds = executionTimeMilliseconds; - this.ThresholdMilliseconds = thresholdMilliseconds; - this.Context = context; - } - } -} diff --git a/src/SMAPI/Framework/PerformanceCounter/PeakEntry.cs b/src/SMAPI/Framework/PerformanceCounter/PeakEntry.cs deleted file mode 100644 index abb29541..00000000 --- a/src/SMAPI/Framework/PerformanceCounter/PeakEntry.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System; - -namespace StardewModdingAPI.Framework.PerformanceCounter -{ - /// A peak invocation time. - internal struct PeakEntry - { - /********* - ** Accessors - *********/ - /// The actual execution time in milliseconds. - public double ExecutionTimeMilliseconds { get; } - - /// When the entry occurred. - public DateTime EventTime { get; } - - /// The sources involved in exceeding the threshold. - public AlertContext[] Context { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The actual execution time in milliseconds. - /// When the entry occurred. - /// The sources involved in exceeding the threshold. - public PeakEntry(double executionTimeMilliseconds, DateTime eventTime, AlertContext[] context) - { - this.ExecutionTimeMilliseconds = executionTimeMilliseconds; - this.EventTime = eventTime; - this.Context = context; - } - } -} diff --git a/src/SMAPI/Framework/PerformanceCounter/PerformanceCounter.cs b/src/SMAPI/Framework/PerformanceCounter/PerformanceCounter.cs deleted file mode 100644 index 0fb6482d..00000000 --- a/src/SMAPI/Framework/PerformanceCounter/PerformanceCounter.cs +++ /dev/null @@ -1,125 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Harmony; - -namespace StardewModdingAPI.Framework.PerformanceCounter -{ - /// Tracks metadata about a particular code event. - internal class PerformanceCounter - { - /********* - ** Fields - *********/ - /// The size of the ring buffer. - private readonly int MaxEntries = 16384; - - /// The collection to which this performance counter belongs. - private readonly PerformanceCounterCollection ParentCollection; - - /// The performance counter entries. - private readonly Stack Entries; - - /// The entry with the highest execution time. - private PerformanceCounterEntry? PeakPerformanceCounterEntry; - - - /********* - ** Accessors - *********/ - /// The name of the source. - public string Source { get; } - - /// The alert threshold in milliseconds - public double AlertThresholdMilliseconds { get; set; } - - /// If alerting is enabled or not - public bool EnableAlerts { get; set; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The collection to which this performance counter belongs. - /// The name of the source. - public PerformanceCounter(PerformanceCounterCollection parentCollection, string source) - { - this.ParentCollection = parentCollection; - this.Source = source; - this.Entries = new Stack(this.MaxEntries); - } - - /// Add a performance counter entry to the list, update monitoring, and raise alerts if needed. - /// The entry to add. - public void Add(PerformanceCounterEntry entry) - { - // add entry - if (this.Entries.Count > this.MaxEntries) - this.Entries.Pop(); - this.Entries.Add(entry); - - // update metrics - if (this.PeakPerformanceCounterEntry == null || entry.ElapsedMilliseconds > this.PeakPerformanceCounterEntry.Value.ElapsedMilliseconds) - this.PeakPerformanceCounterEntry = entry; - - // raise alert - if (this.EnableAlerts && entry.ElapsedMilliseconds > this.AlertThresholdMilliseconds) - this.ParentCollection.AddAlert(entry.ElapsedMilliseconds, this.AlertThresholdMilliseconds, new AlertContext(this.Source, entry.ElapsedMilliseconds)); - } - - /// Clear all performance counter entries and monitoring. - public void Reset() - { - this.Entries.Clear(); - this.PeakPerformanceCounterEntry = null; - } - - /// Get the peak entry. - public PerformanceCounterEntry? GetPeak() - { - return this.PeakPerformanceCounterEntry; - } - - /// Get the entry with the highest execution time. - /// The time range to search. - /// The end time for the , or null for the current time. - public PerformanceCounterEntry? GetPeak(TimeSpan range, DateTime? endTime = null) - { - endTime ??= DateTime.UtcNow; - DateTime startTime = endTime.Value.Subtract(range); - - return this.Entries - .Where(entry => entry.EventTime >= startTime && entry.EventTime <= endTime) - .OrderByDescending(x => x.ElapsedMilliseconds) - .FirstOrDefault(); - } - - /// Get the last entry added to the list. - public PerformanceCounterEntry? GetLastEntry() - { - if (this.Entries.Count == 0) - return null; - - return this.Entries.Peek(); - } - - /// Get the average over a given time span. - /// The time range to search. - /// The end time for the , or null for the current time. - public double GetAverage(TimeSpan range, DateTime? endTime = null) - { - endTime ??= DateTime.UtcNow; - DateTime startTime = endTime.Value.Subtract(range); - - double[] entries = this.Entries - .Where(entry => entry.EventTime >= startTime && entry.EventTime <= endTime) - .Select(p => p.ElapsedMilliseconds) - .ToArray(); - - return entries.Length > 0 - ? entries.Average() - : 0; - } - } -} diff --git a/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterCollection.cs b/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterCollection.cs deleted file mode 100644 index bd13a36e..00000000 --- a/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterCollection.cs +++ /dev/null @@ -1,205 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; - -namespace StardewModdingAPI.Framework.PerformanceCounter -{ - internal class PerformanceCounterCollection - { - /********* - ** Fields - *********/ - /// The number of peak invocations to keep. - private readonly int MaxEntries = 16384; - - /// The sources involved in exceeding alert thresholds. - private readonly List TriggeredPerformanceCounters = new List(); - - /// The stopwatch used to track the invocation time. - private readonly Stopwatch InvocationStopwatch = new Stopwatch(); - - /// The performance counter manager. - private readonly PerformanceMonitor PerformanceMonitor; - - /// The time to calculate average calls per second. - private DateTime CallsPerSecondStart = DateTime.UtcNow; - - /// The number of invocations. - private long CallCount; - - /// The peak invocations. - private readonly Stack PeakInvocations; - - - /********* - ** Accessors - *********/ - /// The associated performance counters. - public IDictionary PerformanceCounters { get; } = new Dictionary(); - - /// The name of this collection. - public string Name { get; } - - /// Whether the source is typically invoked at least once per second. - public bool IsPerformanceCritical { get; } - - /// The alert threshold in milliseconds. - public double AlertThresholdMilliseconds { get; set; } - - /// Whether alerts are enabled. - public bool EnableAlerts { get; set; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The performance counter manager. - /// The name of this collection. - /// Whether the source is typically invoked at least once per second. - public PerformanceCounterCollection(PerformanceMonitor performanceMonitor, string name, bool isPerformanceCritical = false) - { - this.PeakInvocations = new Stack(this.MaxEntries); - this.Name = name; - this.PerformanceMonitor = performanceMonitor; - this.IsPerformanceCritical = isPerformanceCritical; - } - - /// Track a single invocation for a named source. - /// The name of the source. - /// The entry. - public void Track(string source, PerformanceCounterEntry entry) - { - // add entry - if (!this.PerformanceCounters.ContainsKey(source)) - this.PerformanceCounters.Add(source, new PerformanceCounter(this, source)); - this.PerformanceCounters[source].Add(entry); - - // raise alert - if (this.EnableAlerts) - this.TriggeredPerformanceCounters.Add(new AlertContext(source, entry.ElapsedMilliseconds)); - } - - /// Get the average execution time for all non-game internal sources in milliseconds. - /// The interval for which to get the average, relative to now - public double GetModsAverageExecutionTime(TimeSpan interval) - { - return this.PerformanceCounters - .Where(entry => entry.Key != Constants.GamePerformanceCounterName) - .Sum(entry => entry.Value.GetAverage(interval)); - } - - /// Get the overall average execution time in milliseconds. - /// The interval for which to get the average, relative to now - public double GetAverageExecutionTime(TimeSpan interval) - { - return this.PerformanceCounters - .Sum(entry => entry.Value.GetAverage(interval)); - } - - /// Get the average execution time for game-internal sources in milliseconds. - public double GetGameAverageExecutionTime(TimeSpan interval) - { - return this.PerformanceCounters.TryGetValue(Constants.GamePerformanceCounterName, out PerformanceCounter gameExecTime) - ? gameExecTime.GetAverage(interval) - : 0; - } - - /// Get the peak execution time in milliseconds. - /// The time range to search. - /// The end time for the , or null for the current time. - public double GetPeakExecutionTime(TimeSpan range, DateTime? endTime = null) - { - if (this.PeakInvocations.Count == 0) - return 0; - - endTime ??= DateTime.UtcNow; - DateTime startTime = endTime.Value.Subtract(range); - - return this.PeakInvocations - .Where(entry => entry.EventTime >= startTime && entry.EventTime <= endTime) - .OrderByDescending(x => x.ExecutionTimeMilliseconds) - .Select(p => p.ExecutionTimeMilliseconds) - .FirstOrDefault(); - } - - /// Start tracking the invocation of this collection. - public void BeginTrackInvocation() - { - this.TriggeredPerformanceCounters.Clear(); - this.InvocationStopwatch.Reset(); - this.InvocationStopwatch.Start(); - - this.CallCount++; - } - - /// End tracking the invocation of this collection, and raise an alert if needed. - public void EndTrackInvocation() - { - this.InvocationStopwatch.Stop(); - - // add invocation - if (this.PeakInvocations.Count >= this.MaxEntries) - this.PeakInvocations.Pop(); - this.PeakInvocations.Push(new PeakEntry(this.InvocationStopwatch.Elapsed.TotalMilliseconds, DateTime.UtcNow, this.TriggeredPerformanceCounters.ToArray())); - - // raise alert - if (this.EnableAlerts && this.InvocationStopwatch.Elapsed.TotalMilliseconds >= this.AlertThresholdMilliseconds) - this.AddAlert(this.InvocationStopwatch.Elapsed.TotalMilliseconds, this.AlertThresholdMilliseconds, this.TriggeredPerformanceCounters.ToArray()); - } - - /// Add an alert. - /// The execution time in milliseconds. - /// The configured threshold. - /// The sources involved in exceeding the threshold. - public void AddAlert(double executionTimeMilliseconds, double thresholdMilliseconds, AlertContext[] alerts) - { - this.PerformanceMonitor.AddAlert( - new AlertEntry(this, executionTimeMilliseconds, thresholdMilliseconds, alerts) - ); - } - - /// Add an alert. - /// The execution time in milliseconds. - /// The configured threshold. - /// The source involved in exceeding the threshold. - public void AddAlert(double executionTimeMilliseconds, double thresholdMilliseconds, AlertContext alert) - { - this.AddAlert(executionTimeMilliseconds, thresholdMilliseconds, new[] { alert }); - } - - /// Reset the calls per second counter. - public void ResetCallsPerSecond() - { - this.CallCount = 0; - this.CallsPerSecondStart = DateTime.UtcNow; - } - - /// Reset all performance counters in this collection. - public void Reset() - { - this.PeakInvocations.Clear(); - foreach (var counter in this.PerformanceCounters) - counter.Value.Reset(); - } - - /// Reset the performance counter for a specific source. - /// The source name. - public void ResetSource(string source) - { - foreach (var i in this.PerformanceCounters) - if (i.Value.Source.Equals(source, StringComparison.InvariantCultureIgnoreCase)) - i.Value.Reset(); - } - - /// Get the average calls per second. - public long GetAverageCallsPerSecond() - { - long runtimeInSeconds = (long)DateTime.UtcNow.Subtract(this.CallsPerSecondStart).TotalSeconds; - return runtimeInSeconds > 0 - ? this.CallCount / runtimeInSeconds - : 0; - } - } -} diff --git a/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterEntry.cs b/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterEntry.cs deleted file mode 100644 index a1d78fc8..00000000 --- a/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterEntry.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System; - -namespace StardewModdingAPI.Framework.PerformanceCounter -{ - /// A single performance counter entry. - internal struct PerformanceCounterEntry - { - /********* - ** Accessors - *********/ - /// When the entry occurred. - public DateTime EventTime { get; } - - /// The elapsed milliseconds. - public double ElapsedMilliseconds { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// When the entry occurred. - /// The elapsed milliseconds. - public PerformanceCounterEntry(DateTime eventTime, double elapsedMilliseconds) - { - this.EventTime = eventTime; - this.ElapsedMilliseconds = elapsedMilliseconds; - } - } -} diff --git a/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterManager.cs b/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterManager.cs deleted file mode 100644 index 81e4e468..00000000 --- a/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterManager.cs +++ /dev/null @@ -1,184 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Text; -using StardewModdingAPI.Framework.Events; - -namespace StardewModdingAPI.Framework.PerformanceCounter -{ - /// Tracks performance metrics. - internal class PerformanceMonitor - { - /********* - ** Fields - *********/ - /// The recorded alerts. - private readonly IList Alerts = new List(); - - /// The monitor for output logging. - private readonly IMonitor Monitor; - - /// The invocation stopwatch. - private readonly Stopwatch InvocationStopwatch = new Stopwatch(); - - /// The underlying performance counter collections. - private readonly IDictionary Collections = new Dictionary(StringComparer.InvariantCultureIgnoreCase); - - - /********* - ** Accessors - *********/ - /// Whether alerts are paused. - public bool PauseAlerts { get; set; } - - /// Whether performance counter tracking is enabled. - public bool EnableTracking { get; set; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The monitor for output logging. - public PerformanceMonitor(IMonitor monitor) - { - this.Monitor = monitor; - } - - /// Reset all performance counters in all collections. - public void Reset() - { - foreach (PerformanceCounterCollection collection in this.Collections.Values) - collection.Reset(); - } - - /// Track the invocation time for a collection. - /// The name of the collection. - /// The action to execute and track. - public void Track(string collectionName, Action action) - { - if (!this.EnableTracking) - { - action(); - return; - } - - PerformanceCounterCollection collection = this.GetOrCreateCollectionByName(collectionName); - collection.BeginTrackInvocation(); - try - { - action(); - } - finally - { - collection.EndTrackInvocation(); - } - } - - /// Track a single performance counter invocation in a specific collection. - /// The name of the collection. - /// The name of the source. - /// The action to execute and track. - public void Track(string collectionName, string sourceName, Action action) - { - if (!this.EnableTracking) - { - action(); - return; - } - - PerformanceCounterCollection collection = this.GetOrCreateCollectionByName(collectionName); - DateTime eventTime = DateTime.UtcNow; - this.InvocationStopwatch.Reset(); - this.InvocationStopwatch.Start(); - - try - { - action(); - } - finally - { - this.InvocationStopwatch.Stop(); - collection.Track(sourceName, new PerformanceCounterEntry(eventTime, this.InvocationStopwatch.Elapsed.TotalMilliseconds)); - } - } - - /// Reset the performance counters for a specific collection. - /// The collection name. - public void ResetCollection(string name) - { - if (this.Collections.TryGetValue(name, out PerformanceCounterCollection collection)) - { - collection.ResetCallsPerSecond(); - collection.Reset(); - } - } - - /// Reset performance counters for a specific source. - /// The name of the source. - public void ResetSource(string name) - { - foreach (PerformanceCounterCollection performanceCounterCollection in this.Collections.Values) - performanceCounterCollection.ResetSource(name); - } - - /// Print any queued alerts. - public void PrintQueuedAlerts() - { - if (this.Alerts.Count == 0) - return; - - StringBuilder report = new StringBuilder(); - - foreach (AlertEntry alert in this.Alerts) - { - report.AppendLine($"{alert.Collection.Name} took {alert.ExecutionTimeMilliseconds:F2}ms (exceeded threshold of {alert.ThresholdMilliseconds:F2}ms)"); - - foreach (AlertContext context in alert.Context.OrderByDescending(p => p.Elapsed)) - report.AppendLine(context.ToString()); - } - - this.Alerts.Clear(); - this.Monitor.Log(report.ToString(), LogLevel.Error); - } - - /// Add an alert to the queue. - /// The alert to add. - public void AddAlert(AlertEntry entry) - { - if (!this.PauseAlerts) - this.Alerts.Add(entry); - } - - /// Initialize the default performance counter collections. - /// The event manager. - public void InitializePerformanceCounterCollections(EventManager eventManager) - { - foreach (IManagedEvent @event in eventManager.GetAllEvents()) - this.Collections[@event.EventName] = new PerformanceCounterCollection(this, @event.EventName, @event.IsPerformanceCritical); - } - - /// Get the underlying performance counters. - public IEnumerable GetCollections() - { - return this.Collections.Values; - } - - - /********* - ** Public methods - *********/ - /// Get a collection by name and creates it if it doesn't exist. - /// The name of the collection. - private PerformanceCounterCollection GetOrCreateCollectionByName(string name) - { - if (!this.Collections.TryGetValue(name, out PerformanceCounterCollection collection)) - { - collection = new PerformanceCounterCollection(this, name); - this.Collections[name] = collection; - } - return collection; - } - } -} diff --git a/src/SMAPI/Framework/PerformanceMonitoring/AlertContext.cs b/src/SMAPI/Framework/PerformanceMonitoring/AlertContext.cs new file mode 100644 index 00000000..01197f74 --- /dev/null +++ b/src/SMAPI/Framework/PerformanceMonitoring/AlertContext.cs @@ -0,0 +1,34 @@ +namespace StardewModdingAPI.Framework.PerformanceMonitoring +{ + /// The context for an alert. + internal struct AlertContext + { + /********* + ** Accessors + *********/ + /// The source which triggered the alert. + public string Source { get; } + + /// The elapsed milliseconds. + public double Elapsed { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The source which triggered the alert. + /// The elapsed milliseconds. + public AlertContext(string source, double elapsed) + { + this.Source = source; + this.Elapsed = elapsed; + } + + /// Get a human-readable text form of this instance. + public override string ToString() + { + return $"{this.Source}: {this.Elapsed:F2}ms"; + } + } +} diff --git a/src/SMAPI/Framework/PerformanceMonitoring/AlertEntry.cs b/src/SMAPI/Framework/PerformanceMonitoring/AlertEntry.cs new file mode 100644 index 00000000..f5b80189 --- /dev/null +++ b/src/SMAPI/Framework/PerformanceMonitoring/AlertEntry.cs @@ -0,0 +1,38 @@ +namespace StardewModdingAPI.Framework.PerformanceMonitoring +{ + /// A single alert entry. + internal struct AlertEntry + { + /********* + ** Accessors + *********/ + /// The collection in which the alert occurred. + public PerformanceCounterCollection Collection { get; } + + /// The actual execution time in milliseconds. + public double ExecutionTimeMilliseconds { get; } + + /// The configured alert threshold in milliseconds. + public double ThresholdMilliseconds { get; } + + /// The sources involved in exceeding the threshold. + public AlertContext[] Context { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The collection in which the alert occurred. + /// The actual execution time in milliseconds. + /// The configured alert threshold in milliseconds. + /// The sources involved in exceeding the threshold. + public AlertEntry(PerformanceCounterCollection collection, double executionTimeMilliseconds, double thresholdMilliseconds, AlertContext[] context) + { + this.Collection = collection; + this.ExecutionTimeMilliseconds = executionTimeMilliseconds; + this.ThresholdMilliseconds = thresholdMilliseconds; + this.Context = context; + } + } +} diff --git a/src/SMAPI/Framework/PerformanceMonitoring/PeakEntry.cs b/src/SMAPI/Framework/PerformanceMonitoring/PeakEntry.cs new file mode 100644 index 00000000..cff502ad --- /dev/null +++ b/src/SMAPI/Framework/PerformanceMonitoring/PeakEntry.cs @@ -0,0 +1,35 @@ +using System; + +namespace StardewModdingAPI.Framework.PerformanceMonitoring +{ + /// A peak invocation time. + internal struct PeakEntry + { + /********* + ** Accessors + *********/ + /// The actual execution time in milliseconds. + public double ExecutionTimeMilliseconds { get; } + + /// When the entry occurred. + public DateTime EventTime { get; } + + /// The sources involved in exceeding the threshold. + public AlertContext[] Context { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The actual execution time in milliseconds. + /// When the entry occurred. + /// The sources involved in exceeding the threshold. + public PeakEntry(double executionTimeMilliseconds, DateTime eventTime, AlertContext[] context) + { + this.ExecutionTimeMilliseconds = executionTimeMilliseconds; + this.EventTime = eventTime; + this.Context = context; + } + } +} diff --git a/src/SMAPI/Framework/PerformanceMonitoring/PerformanceCounter.cs b/src/SMAPI/Framework/PerformanceMonitoring/PerformanceCounter.cs new file mode 100644 index 00000000..3cf668ee --- /dev/null +++ b/src/SMAPI/Framework/PerformanceMonitoring/PerformanceCounter.cs @@ -0,0 +1,125 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Harmony; + +namespace StardewModdingAPI.Framework.PerformanceMonitoring +{ + /// Tracks metadata about a particular code event. + internal class PerformanceCounter + { + /********* + ** Fields + *********/ + /// The size of the ring buffer. + private readonly int MaxEntries = 16384; + + /// The collection to which this performance counter belongs. + private readonly PerformanceCounterCollection ParentCollection; + + /// The performance counter entries. + private readonly Stack Entries; + + /// The entry with the highest execution time. + private PerformanceCounterEntry? PeakPerformanceCounterEntry; + + + /********* + ** Accessors + *********/ + /// The name of the source. + public string Source { get; } + + /// The alert threshold in milliseconds + public double AlertThresholdMilliseconds { get; set; } + + /// If alerting is enabled or not + public bool EnableAlerts { get; set; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The collection to which this performance counter belongs. + /// The name of the source. + public PerformanceCounter(PerformanceCounterCollection parentCollection, string source) + { + this.ParentCollection = parentCollection; + this.Source = source; + this.Entries = new Stack(this.MaxEntries); + } + + /// Add a performance counter entry to the list, update monitoring, and raise alerts if needed. + /// The entry to add. + public void Add(PerformanceCounterEntry entry) + { + // add entry + if (this.Entries.Count > this.MaxEntries) + this.Entries.Pop(); + this.Entries.Add(entry); + + // update metrics + if (this.PeakPerformanceCounterEntry == null || entry.ElapsedMilliseconds > this.PeakPerformanceCounterEntry.Value.ElapsedMilliseconds) + this.PeakPerformanceCounterEntry = entry; + + // raise alert + if (this.EnableAlerts && entry.ElapsedMilliseconds > this.AlertThresholdMilliseconds) + this.ParentCollection.AddAlert(entry.ElapsedMilliseconds, this.AlertThresholdMilliseconds, new AlertContext(this.Source, entry.ElapsedMilliseconds)); + } + + /// Clear all performance counter entries and monitoring. + public void Reset() + { + this.Entries.Clear(); + this.PeakPerformanceCounterEntry = null; + } + + /// Get the peak entry. + public PerformanceCounterEntry? GetPeak() + { + return this.PeakPerformanceCounterEntry; + } + + /// Get the entry with the highest execution time. + /// The time range to search. + /// The end time for the , or null for the current time. + public PerformanceCounterEntry? GetPeak(TimeSpan range, DateTime? endTime = null) + { + endTime ??= DateTime.UtcNow; + DateTime startTime = endTime.Value.Subtract(range); + + return this.Entries + .Where(entry => entry.EventTime >= startTime && entry.EventTime <= endTime) + .OrderByDescending(x => x.ElapsedMilliseconds) + .FirstOrDefault(); + } + + /// Get the last entry added to the list. + public PerformanceCounterEntry? GetLastEntry() + { + if (this.Entries.Count == 0) + return null; + + return this.Entries.Peek(); + } + + /// Get the average over a given time span. + /// The time range to search. + /// The end time for the , or null for the current time. + public double GetAverage(TimeSpan range, DateTime? endTime = null) + { + endTime ??= DateTime.UtcNow; + DateTime startTime = endTime.Value.Subtract(range); + + double[] entries = this.Entries + .Where(entry => entry.EventTime >= startTime && entry.EventTime <= endTime) + .Select(p => p.ElapsedMilliseconds) + .ToArray(); + + return entries.Length > 0 + ? entries.Average() + : 0; + } + } +} diff --git a/src/SMAPI/Framework/PerformanceMonitoring/PerformanceCounterCollection.cs b/src/SMAPI/Framework/PerformanceMonitoring/PerformanceCounterCollection.cs new file mode 100644 index 00000000..0bb78c74 --- /dev/null +++ b/src/SMAPI/Framework/PerformanceMonitoring/PerformanceCounterCollection.cs @@ -0,0 +1,205 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; + +namespace StardewModdingAPI.Framework.PerformanceMonitoring +{ + internal class PerformanceCounterCollection + { + /********* + ** Fields + *********/ + /// The number of peak invocations to keep. + private readonly int MaxEntries = 16384; + + /// The sources involved in exceeding alert thresholds. + private readonly List TriggeredPerformanceCounters = new List(); + + /// The stopwatch used to track the invocation time. + private readonly Stopwatch InvocationStopwatch = new Stopwatch(); + + /// The performance counter manager. + private readonly PerformanceMonitor PerformanceMonitor; + + /// The time to calculate average calls per second. + private DateTime CallsPerSecondStart = DateTime.UtcNow; + + /// The number of invocations. + private long CallCount; + + /// The peak invocations. + private readonly Stack PeakInvocations; + + + /********* + ** Accessors + *********/ + /// The associated performance counters. + public IDictionary PerformanceCounters { get; } = new Dictionary(); + + /// The name of this collection. + public string Name { get; } + + /// Whether the source is typically invoked at least once per second. + public bool IsPerformanceCritical { get; } + + /// The alert threshold in milliseconds. + public double AlertThresholdMilliseconds { get; set; } + + /// Whether alerts are enabled. + public bool EnableAlerts { get; set; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The performance counter manager. + /// The name of this collection. + /// Whether the source is typically invoked at least once per second. + public PerformanceCounterCollection(PerformanceMonitor performanceMonitor, string name, bool isPerformanceCritical = false) + { + this.PeakInvocations = new Stack(this.MaxEntries); + this.Name = name; + this.PerformanceMonitor = performanceMonitor; + this.IsPerformanceCritical = isPerformanceCritical; + } + + /// Track a single invocation for a named source. + /// The name of the source. + /// The entry. + public void Track(string source, PerformanceCounterEntry entry) + { + // add entry + if (!this.PerformanceCounters.ContainsKey(source)) + this.PerformanceCounters.Add(source, new PerformanceCounter(this, source)); + this.PerformanceCounters[source].Add(entry); + + // raise alert + if (this.EnableAlerts) + this.TriggeredPerformanceCounters.Add(new AlertContext(source, entry.ElapsedMilliseconds)); + } + + /// Get the average execution time for all non-game internal sources in milliseconds. + /// The interval for which to get the average, relative to now + public double GetModsAverageExecutionTime(TimeSpan interval) + { + return this.PerformanceCounters + .Where(entry => entry.Key != Constants.GamePerformanceCounterName) + .Sum(entry => entry.Value.GetAverage(interval)); + } + + /// Get the overall average execution time in milliseconds. + /// The interval for which to get the average, relative to now + public double GetAverageExecutionTime(TimeSpan interval) + { + return this.PerformanceCounters + .Sum(entry => entry.Value.GetAverage(interval)); + } + + /// Get the average execution time for game-internal sources in milliseconds. + public double GetGameAverageExecutionTime(TimeSpan interval) + { + return this.PerformanceCounters.TryGetValue(Constants.GamePerformanceCounterName, out PerformanceCounter gameExecTime) + ? gameExecTime.GetAverage(interval) + : 0; + } + + /// Get the peak execution time in milliseconds. + /// The time range to search. + /// The end time for the , or null for the current time. + public double GetPeakExecutionTime(TimeSpan range, DateTime? endTime = null) + { + if (this.PeakInvocations.Count == 0) + return 0; + + endTime ??= DateTime.UtcNow; + DateTime startTime = endTime.Value.Subtract(range); + + return this.PeakInvocations + .Where(entry => entry.EventTime >= startTime && entry.EventTime <= endTime) + .OrderByDescending(x => x.ExecutionTimeMilliseconds) + .Select(p => p.ExecutionTimeMilliseconds) + .FirstOrDefault(); + } + + /// Start tracking the invocation of this collection. + public void BeginTrackInvocation() + { + this.TriggeredPerformanceCounters.Clear(); + this.InvocationStopwatch.Reset(); + this.InvocationStopwatch.Start(); + + this.CallCount++; + } + + /// End tracking the invocation of this collection, and raise an alert if needed. + public void EndTrackInvocation() + { + this.InvocationStopwatch.Stop(); + + // add invocation + if (this.PeakInvocations.Count >= this.MaxEntries) + this.PeakInvocations.Pop(); + this.PeakInvocations.Push(new PeakEntry(this.InvocationStopwatch.Elapsed.TotalMilliseconds, DateTime.UtcNow, this.TriggeredPerformanceCounters.ToArray())); + + // raise alert + if (this.EnableAlerts && this.InvocationStopwatch.Elapsed.TotalMilliseconds >= this.AlertThresholdMilliseconds) + this.AddAlert(this.InvocationStopwatch.Elapsed.TotalMilliseconds, this.AlertThresholdMilliseconds, this.TriggeredPerformanceCounters.ToArray()); + } + + /// Add an alert. + /// The execution time in milliseconds. + /// The configured threshold. + /// The sources involved in exceeding the threshold. + public void AddAlert(double executionTimeMilliseconds, double thresholdMilliseconds, AlertContext[] alerts) + { + this.PerformanceMonitor.AddAlert( + new AlertEntry(this, executionTimeMilliseconds, thresholdMilliseconds, alerts) + ); + } + + /// Add an alert. + /// The execution time in milliseconds. + /// The configured threshold. + /// The source involved in exceeding the threshold. + public void AddAlert(double executionTimeMilliseconds, double thresholdMilliseconds, AlertContext alert) + { + this.AddAlert(executionTimeMilliseconds, thresholdMilliseconds, new[] { alert }); + } + + /// Reset the calls per second counter. + public void ResetCallsPerSecond() + { + this.CallCount = 0; + this.CallsPerSecondStart = DateTime.UtcNow; + } + + /// Reset all performance counters in this collection. + public void Reset() + { + this.PeakInvocations.Clear(); + foreach (var counter in this.PerformanceCounters) + counter.Value.Reset(); + } + + /// Reset the performance counter for a specific source. + /// The source name. + public void ResetSource(string source) + { + foreach (var i in this.PerformanceCounters) + if (i.Value.Source.Equals(source, StringComparison.InvariantCultureIgnoreCase)) + i.Value.Reset(); + } + + /// Get the average calls per second. + public long GetAverageCallsPerSecond() + { + long runtimeInSeconds = (long)DateTime.UtcNow.Subtract(this.CallsPerSecondStart).TotalSeconds; + return runtimeInSeconds > 0 + ? this.CallCount / runtimeInSeconds + : 0; + } + } +} diff --git a/src/SMAPI/Framework/PerformanceMonitoring/PerformanceCounterEntry.cs b/src/SMAPI/Framework/PerformanceMonitoring/PerformanceCounterEntry.cs new file mode 100644 index 00000000..8adbd88d --- /dev/null +++ b/src/SMAPI/Framework/PerformanceMonitoring/PerformanceCounterEntry.cs @@ -0,0 +1,30 @@ +using System; + +namespace StardewModdingAPI.Framework.PerformanceMonitoring +{ + /// A single performance counter entry. + internal struct PerformanceCounterEntry + { + /********* + ** Accessors + *********/ + /// When the entry occurred. + public DateTime EventTime { get; } + + /// The elapsed milliseconds. + public double ElapsedMilliseconds { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// When the entry occurred. + /// The elapsed milliseconds. + public PerformanceCounterEntry(DateTime eventTime, double elapsedMilliseconds) + { + this.EventTime = eventTime; + this.ElapsedMilliseconds = elapsedMilliseconds; + } + } +} diff --git a/src/SMAPI/Framework/PerformanceMonitoring/PerformanceMonitor.cs b/src/SMAPI/Framework/PerformanceMonitoring/PerformanceMonitor.cs new file mode 100644 index 00000000..dfc4f31a --- /dev/null +++ b/src/SMAPI/Framework/PerformanceMonitoring/PerformanceMonitor.cs @@ -0,0 +1,184 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using StardewModdingAPI.Framework.Events; + +namespace StardewModdingAPI.Framework.PerformanceMonitoring +{ + /// Tracks performance metrics. + internal class PerformanceMonitor + { + /********* + ** Fields + *********/ + /// The recorded alerts. + private readonly IList Alerts = new List(); + + /// The monitor for output logging. + private readonly IMonitor Monitor; + + /// The invocation stopwatch. + private readonly Stopwatch InvocationStopwatch = new Stopwatch(); + + /// The underlying performance counter collections. + private readonly IDictionary Collections = new Dictionary(StringComparer.InvariantCultureIgnoreCase); + + + /********* + ** Accessors + *********/ + /// Whether alerts are paused. + public bool PauseAlerts { get; set; } + + /// Whether performance counter tracking is enabled. + public bool EnableTracking { get; set; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The monitor for output logging. + public PerformanceMonitor(IMonitor monitor) + { + this.Monitor = monitor; + } + + /// Reset all performance counters in all collections. + public void Reset() + { + foreach (PerformanceCounterCollection collection in this.Collections.Values) + collection.Reset(); + } + + /// Track the invocation time for a collection. + /// The name of the collection. + /// The action to execute and track. + public void Track(string collectionName, Action action) + { + if (!this.EnableTracking) + { + action(); + return; + } + + PerformanceCounterCollection collection = this.GetOrCreateCollectionByName(collectionName); + collection.BeginTrackInvocation(); + try + { + action(); + } + finally + { + collection.EndTrackInvocation(); + } + } + + /// Track a single performance counter invocation in a specific collection. + /// The name of the collection. + /// The name of the source. + /// The action to execute and track. + public void Track(string collectionName, string sourceName, Action action) + { + if (!this.EnableTracking) + { + action(); + return; + } + + PerformanceCounterCollection collection = this.GetOrCreateCollectionByName(collectionName); + DateTime eventTime = DateTime.UtcNow; + this.InvocationStopwatch.Reset(); + this.InvocationStopwatch.Start(); + + try + { + action(); + } + finally + { + this.InvocationStopwatch.Stop(); + collection.Track(sourceName, new PerformanceCounterEntry(eventTime, this.InvocationStopwatch.Elapsed.TotalMilliseconds)); + } + } + + /// Reset the performance counters for a specific collection. + /// The collection name. + public void ResetCollection(string name) + { + if (this.Collections.TryGetValue(name, out PerformanceCounterCollection collection)) + { + collection.ResetCallsPerSecond(); + collection.Reset(); + } + } + + /// Reset performance counters for a specific source. + /// The name of the source. + public void ResetSource(string name) + { + foreach (PerformanceCounterCollection performanceCounterCollection in this.Collections.Values) + performanceCounterCollection.ResetSource(name); + } + + /// Print any queued alerts. + public void PrintQueuedAlerts() + { + if (this.Alerts.Count == 0) + return; + + StringBuilder report = new StringBuilder(); + + foreach (AlertEntry alert in this.Alerts) + { + report.AppendLine($"{alert.Collection.Name} took {alert.ExecutionTimeMilliseconds:F2}ms (exceeded threshold of {alert.ThresholdMilliseconds:F2}ms)"); + + foreach (AlertContext context in alert.Context.OrderByDescending(p => p.Elapsed)) + report.AppendLine(context.ToString()); + } + + this.Alerts.Clear(); + this.Monitor.Log(report.ToString(), LogLevel.Error); + } + + /// Add an alert to the queue. + /// The alert to add. + public void AddAlert(AlertEntry entry) + { + if (!this.PauseAlerts) + this.Alerts.Add(entry); + } + + /// Initialize the default performance counter collections. + /// The event manager. + public void InitializePerformanceCounterCollections(EventManager eventManager) + { + foreach (IManagedEvent @event in eventManager.GetAllEvents()) + this.Collections[@event.EventName] = new PerformanceCounterCollection(this, @event.EventName, @event.IsPerformanceCritical); + } + + /// Get the underlying performance counters. + public IEnumerable GetCollections() + { + return this.Collections.Values; + } + + + /********* + ** Public methods + *********/ + /// Get a collection by name and creates it if it doesn't exist. + /// The name of the collection. + private PerformanceCounterCollection GetOrCreateCollectionByName(string name) + { + if (!this.Collections.TryGetValue(name, out PerformanceCounterCollection collection)) + { + collection = new PerformanceCounterCollection(this, name); + this.Collections[name] = collection; + } + return collection; + } + } +} diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index ac89587e..f996ae97 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -23,7 +23,7 @@ using StardewModdingAPI.Framework.Models; using StardewModdingAPI.Framework.ModHelpers; using StardewModdingAPI.Framework.ModLoading; using StardewModdingAPI.Framework.Patching; -using StardewModdingAPI.Framework.PerformanceCounter; +using StardewModdingAPI.Framework.PerformanceMonitoring; using StardewModdingAPI.Framework.Reflection; using StardewModdingAPI.Framework.Serialization; using StardewModdingAPI.Patches; diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index 352859ec..4b346059 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -17,7 +17,7 @@ using StardewModdingAPI.Framework.Content; using StardewModdingAPI.Framework.Events; using StardewModdingAPI.Framework.Input; using StardewModdingAPI.Framework.Networking; -using StardewModdingAPI.Framework.PerformanceCounter; +using StardewModdingAPI.Framework.PerformanceMonitoring; using StardewModdingAPI.Framework.Reflection; using StardewModdingAPI.Framework.StateTracking.Comparers; using StardewModdingAPI.Framework.StateTracking.Snapshots; -- cgit From 805d857e6ee30d422f32ed7da5640b8ac7d562b3 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 26 Jan 2020 20:28:58 -0500 Subject: show warning when using commands while disabled, simplify some commands a bit --- .../Commands/Other/PerformanceCounterCommand.cs | 143 +++++++++------------ 1 file changed, 64 insertions(+), 79 deletions(-) (limited to 'src/SMAPI.Mods.ConsoleCommands/Framework') diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/PerformanceCounterCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/PerformanceCounterCommand.cs index da171a44..d6e36123 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/PerformanceCounterCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/PerformanceCounterCommand.cs @@ -13,6 +13,9 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other /********* ** Fields *********/ + /// The name of the command. + private const string CommandName = "performance"; + /// The available commands. private enum SubCommand { @@ -31,7 +34,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other *********/ /// Construct an instance. public PerformanceCounterCommand() - : base("performance", PerformanceCounterCommand.GetDescription()) { } + : base(CommandName, PerformanceCounterCommand.GetDescription()) { } /// Handle the command. /// Writes messages to the console and log file. @@ -97,27 +100,13 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other /// The command arguments. private void HandleSummarySubCommand(IMonitor monitor, ArgumentParser args) { - if (!args.TryGet(1, "mode", out string mode, false)) - mode = "important"; + if (!this.AssertEnabled(monitor)) + return; IEnumerable data = SCore.PerformanceMonitor.GetCollections(); - switch (mode) - { - case null: - case "important": - data = data.Where(p => p.IsPerformanceCritical); - break; - - case "all": - break; - - default: - data = data.Where(p => p.Name.ToLowerInvariant().Contains(mode.ToLowerInvariant())); - break; - } double? threshold = null; - if (args.TryGetDecimal(2, "threshold", out decimal t, false)) + if (args.TryGetDecimal(1, "threshold", out decimal t, required: false)) threshold = (double?)t; TimeSpan interval = TimeSpan.FromSeconds(60); @@ -147,23 +136,21 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other /// The command arguments. private void HandleDetailSubCommand(IMonitor monitor, ArgumentParser args) { - var collections = new List(); - TimeSpan averageInterval = TimeSpan.FromSeconds(60); - double? thresholdMilliseconds = null; - string sourceFilter = null; + if (!this.AssertEnabled(monitor)) + return; - if (args.TryGet(1, "collection", out string collectionName)) - { - collections.AddRange(SCore.PerformanceMonitor.GetCollections().Where(collection => collection.Name.ToLowerInvariant().Contains(collectionName.ToLowerInvariant()))); + // parse args + double? thresholdMilliseconds = null; + if (args.TryGetDecimal(1, "threshold", out decimal t, required: false)) + thresholdMilliseconds = (double)t; - if (args.Count >= 2 && decimal.TryParse(args[2], out _) && args.TryGetDecimal(2, "threshold", out decimal value, false)) - thresholdMilliseconds = (double?)value; - else if (args.TryGet(2, "source", out string sourceName, false)) - sourceFilter = sourceName; - } + // get collections + var collections = SCore.PerformanceMonitor.GetCollections(); + // format + TimeSpan averageInterval = TimeSpan.FromSeconds(60); foreach (PerformanceCounterCollection c in collections) - this.OutputPerformanceCollectionDetail(monitor, c, averageInterval, thresholdMilliseconds, sourceFilter); + this.OutputPerformanceCollectionDetail(monitor, c, averageInterval, thresholdMilliseconds); } /// Handles the trigger sub command. @@ -171,6 +158,9 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other /// The command arguments. private void HandleTriggerSubCommand(IMonitor monitor, ArgumentParser args) { + if (!this.AssertEnabled(monitor)) + return; + if (args.TryGet(1, "mode", out string mode, false)) { switch (mode) @@ -210,7 +200,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other break; default: - this.LogUsageError(monitor, $"Unknown mode {mode}. See 'pc help trigger' for usage."); + this.LogUsageError(monitor, $"Unknown mode {mode}. See '{CommandName} help trigger' for usage."); break; } } @@ -331,7 +321,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other if (asDump) { foreach (var item in collectionTriggers) - report.AppendLine($"pc trigger {item.CollectionName} {item.Threshold}"); + report.AppendLine($"{CommandName} trigger {item.CollectionName} {item.Threshold}"); } else { @@ -356,7 +346,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other if (asDump) { foreach (SourceTrigger item in sourceTriggers) - report.AppendLine($"pc trigger {item.CollectionName} {item.Threshold} {item.SourceName}"); + report.AppendLine($"{CommandName} trigger {item.CollectionName} {item.Threshold} {item.SourceName}"); } else { @@ -381,6 +371,9 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other /// The command arguments. private void HandleResetSubCommand(IMonitor monitor, ArgumentParser args) { + if (!this.AssertEnabled(monitor)) + return; + if (args.TryGet(1, "type", out string type, false, new[] { "category", "source" })) { args.TryGet(2, "name", out string name); @@ -404,26 +397,17 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other } } - /// Outputs the details for a collection. /// Writes messages to the console and log file. /// The collection. /// The interval over which to calculate the averages. /// The threshold. - /// The source filter. - private void OutputPerformanceCollectionDetail(IMonitor monitor, PerformanceCounterCollection collection, - TimeSpan averageInterval, double? thresholdMilliseconds, string sourceFilter = null) + private void OutputPerformanceCollectionDetail(IMonitor monitor, PerformanceCounterCollection collection, TimeSpan averageInterval, double? thresholdMilliseconds) { StringBuilder report = new StringBuilder($"Performance Counter for {collection.Name}:\n\n"); List> data = collection.PerformanceCounters.ToList(); - if (sourceFilter != null) - { - data = collection.PerformanceCounters.Where(p => - p.Value.Source.ToLowerInvariant().Contains(sourceFilter.ToLowerInvariant())).ToList(); - } - if (thresholdMilliseconds != null) data = data.Where(p => p.Value.GetAverage(averageInterval) >= thresholdMilliseconds).ToList(); @@ -476,46 +460,35 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other switch (subcommand) { case SubCommand.Detail: - report.AppendLine("Usage: pc detail "); - report.AppendLine(" pc detail "); + report.AppendLine($" {CommandName} detail "); report.AppendLine(); report.AppendLine("Displays details for a specific collection."); report.AppendLine(); report.AppendLine("Arguments:"); - report.AppendLine(" Required. The full or partial name of the collection to display."); - report.AppendLine(" Optional. The full or partial name of the source."); report.AppendLine(" Optional. The threshold in milliseconds. Any average execution time below that"); report.AppendLine(" threshold is not reported."); report.AppendLine(); report.AppendLine("Examples:"); - report.AppendLine("pc detail Display.Rendering Displays all performance counters for the 'Display.Rendering' collection"); - report.AppendLine("pc detail Display.Rendering Pathoschild.ChestsAnywhere Displays the 'Display.Rendering' performance counter for 'Pathoschild.ChestsAnywhere'"); - report.AppendLine("pc detail Display.Rendering 5 Displays the 'Display.Rendering' performance counters exceeding an average of 5ms"); + report.AppendLine($"{CommandName} detail 5 Show counters exceeding an average of 5ms"); break; case SubCommand.Summary: - report.AppendLine("Usage: pc summary "); + report.AppendLine($"Usage: {CommandName} summary "); report.AppendLine(); report.AppendLine("Displays the performance counter summary."); report.AppendLine(); report.AppendLine("Arguments:"); - report.AppendLine(" Optional. Defaults to 'important' if omitted. Specifies one of these modes:"); - report.AppendLine(" - all Displays performance counters from all collections"); - report.AppendLine(" - important Displays only important performance counter collections"); - report.AppendLine(); - report.AppendLine(" Optional. Only shows performance counter collections matching the given name"); - report.AppendLine(" Optional. Hides the actual execution time if it is below this threshold"); + report.AppendLine(" Optional. Hides the actual execution time if it's below this threshold"); report.AppendLine(); report.AppendLine("Examples:"); - report.AppendLine("pc summary all Shows all events"); - report.AppendLine("pc summary all 5 Shows all events"); - report.AppendLine("pc summary Display.Rendering Shows only the 'Display.Rendering' collection"); + report.AppendLine($"{CommandName} summary Show all events"); + report.AppendLine($"{CommandName} summary 5 Shows events exceeding an average of 5ms"); break; case SubCommand.Trigger: - report.AppendLine("Usage: pc trigger "); - report.AppendLine("Usage: pc trigger collection "); - report.AppendLine("Usage: pc trigger collection "); + report.AppendLine($"Usage: {CommandName} trigger "); + report.AppendLine($"Usage: {CommandName} trigger collection "); + report.AppendLine($"Usage: {CommandName} trigger collection "); report.AppendLine(); report.AppendLine("Manages alert triggers."); report.AppendLine(); @@ -540,23 +513,23 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other report.AppendLine(); report.AppendLine("Examples:"); report.AppendLine(); - report.AppendLine("pc trigger collection Display.Rendering 10"); + report.AppendLine($"{CommandName} trigger collection Display.Rendering 10"); report.AppendLine(" Sets up an alert trigger which writes on the console if the execution time of all performance counters in"); report.AppendLine(" the 'Display.Rendering' collection exceed 10 milliseconds."); report.AppendLine(); - report.AppendLine("pc trigger collection Display.Rendering 5 Pathoschild.ChestsAnywhere"); + report.AppendLine($"{CommandName} trigger collection Display.Rendering 5 Pathoschild.ChestsAnywhere"); report.AppendLine(" Sets up an alert trigger to write on the console if the execution time of Pathoschild.ChestsAnywhere in"); report.AppendLine(" the 'Display.Rendering' collection exceed 5 milliseconds."); report.AppendLine(); - report.AppendLine("pc trigger collection Display.Rendering 0"); + report.AppendLine($"{CommandName} trigger collection Display.Rendering 0"); report.AppendLine(" Removes the threshold previously defined from the collection. Note that source-specific thresholds are left intact."); report.AppendLine(); - report.AppendLine("pc trigger clear"); + report.AppendLine($"{CommandName} trigger clear"); report.AppendLine(" Clears all previously setup alert triggers."); break; case SubCommand.Reset: - report.AppendLine("Usage: pc reset "); + report.AppendLine($"Usage: {CommandName} reset "); report.AppendLine(); report.AppendLine("Resets performance counters."); report.AppendLine(); @@ -571,9 +544,9 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other report.AppendLine(" or the source. The name must be an exact match."); report.AppendLine(); report.AppendLine("Examples:"); - report.AppendLine("pc reset Resets all performance counters"); - report.AppendLine("pc reset source Pathoschild.ChestsAnywhere Resets all performance for the source named Pathoschild.ChestsAnywhere"); - report.AppendLine("pc reset collection Display.Rendering Resets all performance for the collection named Display.Rendering"); + report.AppendLine($"{CommandName} reset Resets all performance counters"); + report.AppendLine($"{CommandName} reset source Pathoschild.ChestsAnywhere Resets all performance for the source named Pathoschild.ChestsAnywhere"); + report.AppendLine($"{CommandName} reset collection Display.Rendering Resets all performance for the collection named Display.Rendering"); break; } @@ -586,14 +559,12 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other { StringBuilder report = new StringBuilder(); - report.AppendLine("Displays or configures performance monitoring for diagnose issues."); + report.AppendLine("Displays or configures performance monitoring to diagnose issues. Performance monitoring is disabled by default."); report.AppendLine(); - report.AppendLine("A 'performance counter' is a metric which measures execution time across a range of time for a source (e.g. a mod)."); - report.AppendLine("A set of performance counters is organized in a collection to group various areas."); - report.AppendLine("For example, the performance counter collection named 'Display.Rendered' contains one performance"); + report.AppendLine("For example, the counter collection named 'Display.Rendered' contains one performance"); report.AppendLine("counter when the game executes the 'Display.Rendered' event, and another counter for each mod which handles it."); report.AppendLine(); - report.AppendLine("Usage: pc "); + report.AppendLine($"Usage: {CommandName} "); report.AppendLine(); report.AppendLine("Commands:"); report.AppendLine(); @@ -605,8 +576,8 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other report.AppendLine(" disable Disable performance counter recording."); report.AppendLine(" help Show verbose help for the available commands."); report.AppendLine(); - report.AppendLine("To get help for a specific command, use 'pc help ', for example:"); - report.AppendLine("pc help summary"); + report.AppendLine($"To get help for a specific command, use '{CommandName} help ', for example:"); + report.AppendLine($"{CommandName} help summary"); report.AppendLine(); report.AppendLine("Defaults to summary if no command is given."); report.AppendLine(); @@ -614,6 +585,20 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other return report.ToString(); } + /// Log a warning if performance monitoring isn't enabled. + /// Writes messages to the console and log file. + /// Returns whether performance monitoring is enabled. + private bool AssertEnabled(IMonitor monitor) + { + if (!SCore.PerformanceMonitor.EnableTracking) + { + monitor.Log($"Performance monitoring is currently disabled; enter '{CommandName} enable' to enable it.", LogLevel.Warn); + return false; + } + + return true; + } + /********* ** Private models -- cgit From 860b30443ec47ceb271a008c26f3b358cf7bb409 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 26 Jan 2020 20:42:28 -0500 Subject: simplify performance details output --- .../Commands/Other/PerformanceCounterCommand.cs | 85 ++++++++++------------ 1 file changed, 38 insertions(+), 47 deletions(-) (limited to 'src/SMAPI.Mods.ConsoleCommands/Framework') diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/PerformanceCounterCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/PerformanceCounterCommand.cs index d6e36123..63851c9d 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/PerformanceCounterCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/PerformanceCounterCommand.cs @@ -140,17 +140,47 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other return; // parse args - double? thresholdMilliseconds = null; + double thresholdMilliseconds = 0; if (args.TryGetDecimal(1, "threshold", out decimal t, required: false)) thresholdMilliseconds = (double)t; // get collections var collections = SCore.PerformanceMonitor.GetCollections(); - // format + // render TimeSpan averageInterval = TimeSpan.FromSeconds(60); - foreach (PerformanceCounterCollection c in collections) - this.OutputPerformanceCollectionDetail(monitor, c, averageInterval, thresholdMilliseconds); + StringBuilder report = new StringBuilder($"Showing details for performance counters of {thresholdMilliseconds}+ milliseconds:\n\n"); + bool anyShown = false; + foreach (PerformanceCounterCollection collection in collections) + { + KeyValuePair[] data = collection.PerformanceCounters + .Where(p => p.Value.GetAverage(averageInterval) >= thresholdMilliseconds) + .ToArray(); + + if (data.Any()) + { + anyShown = true; + report.AppendLine($"{collection.Name}:"); + report.AppendLine(this.GetTableString( + data: data, + header: new[] { "Mod", $"Avg Exec Time (last {(int)averageInterval.TotalSeconds}s)", "Last Exec Time", "Peak Exec Time", $"Peak Exec Time (last {(int)averageInterval.TotalSeconds}s)" }, + getRow: item => new[] + { + item.Key, + this.FormatMilliseconds(item.Value.GetAverage(averageInterval), thresholdMilliseconds), + this.FormatMilliseconds(item.Value.GetLastEntry()?.ElapsedMilliseconds), + this.FormatMilliseconds(item.Value.GetPeak()?.ElapsedMilliseconds), + this.FormatMilliseconds(item.Value.GetPeak(averageInterval)?.ElapsedMilliseconds) + }, + true + )); + } + } + + if (!anyShown) + report.AppendLine("No performance counters found."); + + monitor.Log(report.ToString(), LogLevel.Info); } /// Handles the trigger sub command. @@ -397,45 +427,6 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other } } - /// Outputs the details for a collection. - /// Writes messages to the console and log file. - /// The collection. - /// The interval over which to calculate the averages. - /// The threshold. - private void OutputPerformanceCollectionDetail(IMonitor monitor, PerformanceCounterCollection collection, TimeSpan averageInterval, double? thresholdMilliseconds) - { - StringBuilder report = new StringBuilder($"Performance Counter for {collection.Name}:\n\n"); - - List> data = collection.PerformanceCounters.ToList(); - - if (thresholdMilliseconds != null) - data = data.Where(p => p.Value.GetAverage(averageInterval) >= thresholdMilliseconds).ToList(); - - if (data.Any()) - { - report.AppendLine(this.GetTableString( - data: data, - header: new[] { "Mod", $"Avg Exec Time (last {(int)averageInterval.TotalSeconds}s)", "Last Exec Time", "Peak Exec Time", $"Peak Exec Time (last {(int)averageInterval.TotalSeconds}s)" }, - getRow: item => new[] - { - item.Key, - this.FormatMilliseconds(item.Value.GetAverage(averageInterval), thresholdMilliseconds), - this.FormatMilliseconds(item.Value.GetLastEntry()?.ElapsedMilliseconds), - this.FormatMilliseconds(item.Value.GetPeak()?.ElapsedMilliseconds), - this.FormatMilliseconds(item.Value.GetPeak(averageInterval)?.ElapsedMilliseconds) - }, - true - )); - } - else - { - report.Clear(); - report.AppendLine($"Performance Counter for {collection.Name}: none."); - } - - monitor.Log(report.ToString(), LogLevel.Info); - } - /// Formats the given milliseconds value into a string format. Optionally /// allows a threshold to return "-" if the value is less than the threshold. /// The milliseconds to format. Returns "-" if null @@ -443,10 +434,10 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other /// The formatted milliseconds. private string FormatMilliseconds(double? milliseconds, double? thresholdMilliseconds = null) { - if (milliseconds == null || (thresholdMilliseconds != null && milliseconds < thresholdMilliseconds)) - return "-"; - - return ((double)milliseconds).ToString("F2"); + thresholdMilliseconds ??= 1; + return milliseconds != null && milliseconds >= thresholdMilliseconds + ? ((double)milliseconds).ToString("F2") + : "-"; } /// Shows detailed help for a specific sub command. -- cgit