summaryrefslogtreecommitdiff
path: root/src/SMAPI.Mods.ConsoleCommands
diff options
context:
space:
mode:
authorJesse Plamondon-Willard <Pathoschild@users.noreply.github.com>2020-01-26 20:45:27 -0500
committerGitHub <noreply@github.com>2020-01-26 20:45:27 -0500
commit7cbf298bd42606b41386f117566c79732f88cccf (patch)
treeb474fe278501ff8f24c5bb6534005077b75f45e6 /src/SMAPI.Mods.ConsoleCommands
parenta96bfea205ab855913726cad58d63191fbf24399 (diff)
parent860b30443ec47ceb271a008c26f3b358cf7bb409 (diff)
downloadSMAPI-7cbf298bd42606b41386f117566c79732f88cccf.tar.gz
SMAPI-7cbf298bd42606b41386f117566c79732f88cccf.tar.bz2
SMAPI-7cbf298bd42606b41386f117566c79732f88cccf.zip
Merge pull request #690 from Drachenkaetzchen/performance-counter
Performance counters
Diffstat (limited to 'src/SMAPI.Mods.ConsoleCommands')
-rw-r--r--src/SMAPI.Mods.ConsoleCommands/Framework/Commands/ArgumentParser.cs59
-rw-r--r--src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/PerformanceCounterCommand.cs647
-rw-r--r--src/SMAPI.Mods.ConsoleCommands/Framework/Commands/TrainerCommand.cs8
3 files changed, 703 insertions, 11 deletions
diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/ArgumentParser.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/ArgumentParser.cs
index 10007b42..9c7082c9 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;
using System.Collections;
using System.Collections.Generic;
+using System.Globalization;
using System.Linq;
namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands
@@ -31,13 +32,6 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands
/// <param name="index">The zero-based index of the element to get.</param>
public string this[int index] => this.Args[index];
- /// <summary>A method which parses a string argument into the given value.</summary>
- /// <typeparam name="T">The expected argument type.</typeparam>
- /// <param name="input">The argument to parse.</param>
- /// <param name="output">The parsed value.</param>
- /// <returns>Returns whether the argument was successfully parsed.</returns>
- public delegate bool ParseDelegate<T>(string input, out T output);
-
/*********
** Public methods
@@ -113,6 +107,38 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands
return true;
}
+ /// <summary>Try to read a decimal argument.</summary>
+ /// <param name="index">The argument index.</param>
+ /// <param name="name">The argument name for error messages.</param>
+ /// <param name="value">The parsed value.</param>
+ /// <param name="required">Whether to show an error if the argument is missing.</param>
+ /// <param name="min">The minimum value allowed.</param>
+ /// <param name="max">The maximum value allowed.</param>
+ 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;
+ }
+
/// <summary>Returns an enumerator that iterates through the collection.</summary>
/// <returns>An enumerator that can be used to iterate through the collection.</returns>
public IEnumerator<string> GetEnumerator()
@@ -154,5 +180,22 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands
else
this.LogError($"Argument {index} ({name}) must be an integer.");
}
+
+ /// <summary>Print an error for an invalid decimal argument.</summary>
+ /// <param name="index">The argument index.</param>
+ /// <param name="name">The argument name for error messages.</param>
+ /// <param name="min">The minimum value allowed.</param>
+ /// <param name="max">The maximum value allowed.</param>
+ 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
new file mode 100644
index 00000000..63851c9d
--- /dev/null
+++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/PerformanceCounterCommand.cs
@@ -0,0 +1,647 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using StardewModdingAPI.Framework;
+using StardewModdingAPI.Framework.PerformanceMonitoring;
+
+namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other
+{
+ /// <summary>A set of commands which displays or configures performance monitoring.</summary>
+ internal class PerformanceCounterCommand : TrainerCommand
+ {
+ /*********
+ ** Fields
+ *********/
+ /// <summary>The name of the command.</summary>
+ private const string CommandName = "performance";
+
+ /// <summary>The available commands.</summary>
+ private enum SubCommand
+ {
+ Summary,
+ Detail,
+ Reset,
+ Trigger,
+ Enable,
+ Disable,
+ Help
+ }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ public PerformanceCounterCommand()
+ : base(CommandName, PerformanceCounterCommand.GetDescription()) { }
+
+ /// <summary>Handle the command.</summary>
+ /// <param name="monitor">Writes messages to the console and log file.</param>
+ /// <param name="command">The command name.</param>
+ /// <param name="args">The command arguments.</param>
+ public override void Handle(IMonitor monitor, string command, ArgumentParser args)
+ {
+ // parse args
+ SubCommand subcommand = SubCommand.Summary;
+ {
+ if (args.TryGet(0, "command", out string subcommandStr, false) && !Enum.TryParse(subcommandStr, ignoreCase: true, out subcommand))
+ {
+ this.LogUsageError(monitor, $"Unknown command {subcommandStr}");
+ return;
+ }
+ }
+
+ // 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
+ *********/
+ /// <summary>Handles the summary sub command.</summary>
+ /// <param name="monitor">Writes messages to the console and log file.</param>
+ /// <param name="args">The command arguments.</param>
+ private void HandleSummarySubCommand(IMonitor monitor, ArgumentParser args)
+ {
+ if (!this.AssertEnabled(monitor))
+ return;
+
+ IEnumerable<PerformanceCounterCollection> data = SCore.PerformanceMonitor.GetCollections();
+
+ double? threshold = null;
+ if (args.TryGetDecimal(1, "threshold", out decimal t, required: false))
+ threshold = (double?)t;
+
+ TimeSpan interval = TimeSpan.FromSeconds(60);
+
+ 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" },
+ getRow: item => new[]
+ {
+ item.Name,
+ item.GetAverageCallsPerSecond().ToString(),
+ 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
+ ));
+
+ monitor.Log(report.ToString(), LogLevel.Info);
+ }
+
+ /// <summary>Handles the detail sub command.</summary>
+ /// <param name="monitor">Writes messages to the console and log file.</param>
+ /// <param name="args">The command arguments.</param>
+ private void HandleDetailSubCommand(IMonitor monitor, ArgumentParser args)
+ {
+ if (!this.AssertEnabled(monitor))
+ return;
+
+ // parse args
+ double thresholdMilliseconds = 0;
+ if (args.TryGetDecimal(1, "threshold", out decimal t, required: false))
+ thresholdMilliseconds = (double)t;
+
+ // get collections
+ var collections = SCore.PerformanceMonitor.GetCollections();
+
+ // render
+ TimeSpan averageInterval = TimeSpan.FromSeconds(60);
+ StringBuilder report = new StringBuilder($"Showing details for performance counters of {thresholdMilliseconds}+ milliseconds:\n\n");
+ bool anyShown = false;
+ foreach (PerformanceCounterCollection collection in collections)
+ {
+ KeyValuePair<string, PerformanceCounter>[] 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);
+ }
+
+ /// <summary>Handles the trigger sub command.</summary>
+ /// <param name="monitor">Writes messages to the console and log file.</param>
+ /// <param name="args">The command arguments.</param>
+ private void HandleTriggerSubCommand(IMonitor monitor, ArgumentParser args)
+ {
+ if (!this.AssertEnabled(monitor))
+ return;
+
+ if (args.TryGet(1, "mode", out string mode, false))
+ {
+ switch (mode)
+ {
+ 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, required: false))
+ source = null;
+ this.ConfigureAlertTrigger(monitor, collectionName, source, threshold);
+ }
+ }
+ break;
+
+ case "pause":
+ SCore.PerformanceMonitor.PauseAlerts = true;
+ monitor.Log("Alerts are now paused.", LogLevel.Info);
+ break;
+
+ case "resume":
+ 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 '{CommandName} help trigger' for usage.");
+ break;
+ }
+ }
+ else
+ this.OutputAlertTriggers(monitor);
+ }
+
+ /// <summary>Sets up an an alert trigger.</summary>
+ /// <param name="monitor">Writes messages to the console and log file.</param>
+ /// <param name="collectionName">The name of the collection.</param>
+ /// <param name="sourceName">The name of the source, or null for all sources.</param>
+ /// <param name="threshold">The trigger threshold, or 0 to remove.</param>
+ private void ConfigureAlertTrigger(IMonitor monitor, string collectionName, string sourceName, decimal threshold)
+ {
+ foreach (PerformanceCounterCollection collection in SCore.PerformanceMonitor.GetCollections())
+ {
+ if (collection.Name.ToLowerInvariant().Equals(collectionName.ToLowerInvariant()))
+ {
+ if (sourceName == null)
+ {
+ 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
+ {
+ foreach (var performanceCounter in collection.PerformanceCounters)
+ {
+ if (performanceCounter.Value.Source.ToLowerInvariant().Equals(sourceName.ToLowerInvariant()))
+ {
+ 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;
+ }
+ }
+
+ monitor.Log($"Could not find the source '{sourceName}' in collection '{collectionName}'", LogLevel.Warn);
+ return;
+ }
+ }
+ }
+
+ monitor.Log($"Could not find the collection '{collectionName}'", LogLevel.Warn);
+ }
+
+
+ /// <summary>Clears alert triggering for all collections.</summary>
+ /// <param name="monitor">Writes messages to the console and log file.</param>
+ private void ClearAlertTriggers(IMonitor monitor)
+ {
+ int clearedTriggers = 0;
+ foreach (PerformanceCounterCollection collection in SCore.PerformanceMonitor.GetCollections())
+ {
+ if (collection.EnableAlerts)
+ {
+ collection.EnableAlerts = false;
+ clearedTriggers++;
+ }
+
+ foreach (var performanceCounter in collection.PerformanceCounters)
+ {
+ if (performanceCounter.Value.EnableAlerts)
+ {
+ performanceCounter.Value.EnableAlerts = false;
+ clearedTriggers++;
+ }
+ }
+
+ }
+
+ monitor.Log($"Cleared {clearedTriggers} alert triggers.", LogLevel.Info);
+ }
+
+ /// <summary>Lists all configured alert triggers.</summary>
+ /// <param name="monitor">Writes messages to the console and log file.</param>
+ /// <param name="asDump">True to dump the triggers as commands.</param>
+ private void OutputAlertTriggers(IMonitor monitor, bool asDump = false)
+ {
+ StringBuilder report = new StringBuilder();
+ report.AppendLine("Configured triggers:");
+ report.AppendLine();
+ var collectionTriggers = new List<CollectionTrigger>();
+ var sourceTriggers = new List<SourceTrigger>();
+
+ foreach (PerformanceCounterCollection collection in SCore.PerformanceMonitor.GetCollections())
+ {
+ if (collection.EnableAlerts)
+ collectionTriggers.Add(new CollectionTrigger(collection.Name, collection.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)
+ {
+ report.AppendLine("Collection Triggers:");
+ report.AppendLine();
+
+ if (asDump)
+ {
+ foreach (var item in collectionTriggers)
+ report.AppendLine($"{CommandName} trigger {item.CollectionName} {item.Threshold}");
+ }
+ else
+ {
+ report.AppendLine(this.GetTableString(
+ data: collectionTriggers,
+ header: new[] { "Collection", "Threshold" },
+ getRow: item => new[] { item.CollectionName, this.FormatMilliseconds(item.Threshold) },
+ true
+ ));
+ }
+
+ report.AppendLine();
+ }
+ else
+ report.AppendLine("No collection triggers.");
+
+ if (sourceTriggers.Count > 0)
+ {
+ report.AppendLine("Source Triggers:");
+ report.AppendLine();
+
+ if (asDump)
+ {
+ foreach (SourceTrigger item in sourceTriggers)
+ report.AppendLine($"{CommandName} trigger {item.CollectionName} {item.Threshold} {item.SourceName}");
+ }
+ else
+ {
+ report.AppendLine(this.GetTableString(
+ data: sourceTriggers,
+ header: new[] { "Collection", "Source", "Threshold" },
+ getRow: item => new[] { item.CollectionName, item.SourceName, this.FormatMilliseconds(item.Threshold) },
+ true
+ ));
+ }
+
+ report.AppendLine();
+ }
+ else
+ report.AppendLine("No source triggers.");
+
+ monitor.Log(report.ToString(), LogLevel.Info);
+ }
+
+ /// <summary>Handles the reset sub command.</summary>
+ /// <param name="monitor">Writes messages to the console and log file.</param>
+ /// <param name="args">The command arguments.</param>
+ 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);
+
+ switch (type)
+ {
+ case "category":
+ SCore.PerformanceMonitor.ResetCollection(name);
+ monitor.Log($"All performance counters for category {name} are now cleared.", LogLevel.Info);
+ break;
+ case "source":
+ SCore.PerformanceMonitor.ResetSource(name);
+ monitor.Log($"All performance counters for source {name} are now cleared.", LogLevel.Info);
+ break;
+ }
+ }
+ else
+ {
+ SCore.PerformanceMonitor.Reset();
+ monitor.Log("All performance counters are now cleared.", LogLevel.Info);
+ }
+ }
+
+ /// <summary>Formats the given milliseconds value into a string format. Optionally
+ /// allows a threshold to return "-" if the value is less than the threshold.</summary>
+ /// <param name="milliseconds">The milliseconds to format. Returns "-" if null</param>
+ /// <param name="thresholdMilliseconds">The threshold. Any value below this is returned as "-".</param>
+ /// <returns>The formatted milliseconds.</returns>
+ private string FormatMilliseconds(double? milliseconds, double? thresholdMilliseconds = null)
+ {
+ thresholdMilliseconds ??= 1;
+ return milliseconds != null && milliseconds >= thresholdMilliseconds
+ ? ((double)milliseconds).ToString("F2")
+ : "-";
+ }
+
+ /// <summary>Shows detailed help for a specific sub command.</summary>
+ /// <param name="monitor">The output monitor.</param>
+ /// <param name="subcommand">The subcommand.</param>
+ private void OutputHelp(IMonitor monitor, SubCommand? subcommand)
+ {
+ StringBuilder report = new StringBuilder();
+ report.AppendLine();
+
+ switch (subcommand)
+ {
+ case SubCommand.Detail:
+ report.AppendLine($" {CommandName} detail <threshold>");
+ report.AppendLine();
+ report.AppendLine("Displays details for a specific collection.");
+ report.AppendLine();
+ report.AppendLine("Arguments:");
+ report.AppendLine(" <threshold> Optional. The threshold in milliseconds. Any average execution time below that");
+ report.AppendLine(" threshold is not reported.");
+ report.AppendLine();
+ report.AppendLine("Examples:");
+ report.AppendLine($"{CommandName} detail 5 Show counters exceeding an average of 5ms");
+ break;
+
+ case SubCommand.Summary:
+ report.AppendLine($"Usage: {CommandName} summary <threshold>");
+ report.AppendLine();
+ report.AppendLine("Displays the performance counter summary.");
+ report.AppendLine();
+ report.AppendLine("Arguments:");
+ report.AppendLine(" <threshold> Optional. Hides the actual execution time if it's below this threshold");
+ report.AppendLine();
+ report.AppendLine("Examples:");
+ 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: {CommandName} trigger <mode>");
+ report.AppendLine($"Usage: {CommandName} trigger collection <collectionName> <threshold>");
+ report.AppendLine($"Usage: {CommandName} trigger collection <collectionName> <threshold> <sourceName>");
+ report.AppendLine();
+ report.AppendLine("Manages alert triggers.");
+ report.AppendLine();
+ report.AppendLine("Arguments:");
+ report.AppendLine(" <mode> 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(" <collectionName> 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(" <sourceName> Optional. Specifies the name of a specific source. Must be an exact match.");
+ report.AppendLine();
+ report.AppendLine(" <threshold> 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($"{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($"{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($"{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($"{CommandName} trigger clear");
+ report.AppendLine(" Clears all previously setup alert triggers.");
+ break;
+
+ case SubCommand.Reset:
+ report.AppendLine($"Usage: {CommandName} reset <type> <name>");
+ report.AppendLine();
+ report.AppendLine("Resets performance counters.");
+ report.AppendLine();
+ report.AppendLine("Arguments:");
+ report.AppendLine(" <type> 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(" <name> Required if a <type> 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($"{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;
+ }
+
+ report.AppendLine();
+ monitor.Log(report.ToString(), LogLevel.Info);
+ }
+
+ /// <summary>Get the command description.</summary>
+ private static string GetDescription()
+ {
+ StringBuilder report = new StringBuilder();
+
+ report.AppendLine("Displays or configures performance monitoring to diagnose issues. Performance monitoring is disabled by default.");
+ report.AppendLine();
+ 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: {CommandName} <command> <action>");
+ 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 '{CommandName} help <command>', for example:");
+ report.AppendLine($"{CommandName} help summary");
+ report.AppendLine();
+ report.AppendLine("Defaults to summary if no command is given.");
+ report.AppendLine();
+
+ return report.ToString();
+ }
+
+ /// <summary>Log a warning if performance monitoring isn't enabled.</summary>
+ /// <param name="monitor">Writes messages to the console and log file.</param>
+ /// <returns>Returns whether performance monitoring is enabled.</returns>
+ 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
+ *********/
+ /// <summary>An alert trigger for a collection.</summary>
+ private class CollectionTrigger
+ {
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>The collection name.</summary>
+ public string CollectionName { get; }
+
+ /// <summary>The trigger threshold.</summary>
+ public double Threshold { get; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="collectionName">The collection name.</param>
+ /// <param name="threshold">The trigger threshold.</param>
+ public CollectionTrigger(string collectionName, double threshold)
+ {
+ this.CollectionName = collectionName;
+ this.Threshold = threshold;
+ }
+ }
+
+ /// <summary>An alert triggered for a source.</summary>
+ private class SourceTrigger : CollectionTrigger
+ {
+ /*********
+ ** Accessors
+ *********/
+ /// <summary>The source name.</summary>
+ public string SourceName { get; }
+
+
+ /*********
+ ** Public methods
+ *********/
+ /// <summary>Construct an instance.</summary>
+ /// <param name="collectionName">The collection name.</param>
+ /// <param name="sourceName">The source name.</param>
+ /// <param name="threshold">The trigger threshold.</param>
+ 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 6d5cae97..77a26c6a 100644
--- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/TrainerCommand.cs
+++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/TrainerCommand.cs
@@ -78,7 +78,8 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands
/// <param name="data">The data to display.</param>
/// <param name="header">The table header.</param>
/// <param name="getRow">Returns a set of fields for a data value.</param>
- protected string GetTableString<T>(IEnumerable<T> data, string[] header, Func<T, string[]> getRow)
+ /// <param name="rightAlign">Whether to right-align the data.</param>
+ protected string GetTableString<T>(IEnumerable<T> data, string[] header, Func<T, string[]> getRow, bool rightAlign = false)
{
// get table data
int[] widths = header.Select(p => p.Length).ToArray();
@@ -106,8 +107,9 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands
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], ' '))
+ ))
);
}
}