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') 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 + 3 files changed, 598 insertions(+), 2 deletions(-) (limited to 'src/SMAPI.Mods.ConsoleCommands') 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 @@ + + + + -- 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 +++++++++++----------- 1 file changed, 15 insertions(+), 15 deletions(-) (limited to 'src/SMAPI.Mods.ConsoleCommands') 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) } )); -- 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') 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 ++++++++++++--------- 1 file changed, 350 insertions(+), 246 deletions(-) (limited to 'src/SMAPI.Mods.ConsoleCommands') 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 c