diff options
author | Jesse Plamondon-Willard <Pathoschild@users.noreply.github.com> | 2020-01-26 19:49:17 -0500 |
---|---|---|
committer | Jesse Plamondon-Willard <Pathoschild@users.noreply.github.com> | 2020-01-26 19:49:17 -0500 |
commit | 22a0a32b6d959946bfd80bf0ca9796378f36e0cd (patch) | |
tree | 4dc91461e2a06148c975d12d46986a6a9648e1ff /src | |
parent | 1b905205a3073c56e29c46b5e57c4a9cb2ca5832 (diff) | |
download | SMAPI-22a0a32b6d959946bfd80bf0ca9796378f36e0cd.tar.gz SMAPI-22a0a32b6d959946bfd80bf0ca9796378f36e0cd.tar.bz2 SMAPI-22a0a32b6d959946bfd80bf0ca9796378f36e0cd.zip |
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.
Diffstat (limited to 'src')
20 files changed, 706 insertions, 821 deletions
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 /// <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 @@ -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; - } - /// <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> 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 + /// <summary>A set of commands which displays or configures performance monitoring.</summary> internal class PerformanceCounterCommand : TrainerCommand { - /// <summary>The command names and aliases</summary> - private readonly Dictionary<SubCommand, string[]> SubCommandNames = new Dictionary<SubCommand, string[]>() - { - {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"}}, - }; - - /// <summary>The available commands enum</summary> + /********* + ** Fields + *********/ + /// <summary>The available commands.</summary> 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 + *********/ /// <summary>Construct an instance.</summary> - public PerformanceCounterCommand() : base("pc", PerformanceCounterCommand.GetDescription()) - { - } + public PerformanceCounterCommand() + : base("performance", PerformanceCounterCommand.GetDescription()) { } /// <summary>Handle the command.</summary> /// <param name="monitor">Writes messages to the console and log file.</param> @@ -50,92 +39,94 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other /// <param name="args">The command arguments.</param> 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 + *********/ /// <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) { - IEnumerable<PerformanceCounterCollection> data; - if (!args.TryGet(1, "mode", out string mode, false)) - { mode = "important"; - } + IEnumerable<PerformanceCounterCollection> 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); } /// <summary>Handles the detail sub command.</summary> @@ -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); - } } /// <summary>Handles the trigger sub command.</summary> @@ -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); - } } /// <summary>Sets up an an alert trigger.</summary> @@ -246,7 +225,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other /// <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.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 /// <param name="asDump">True to dump the triggers as commands.</param> 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<CollectionTrigger>(); + var sourceTriggers = new List<SourceTrigger>(); - 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); } /// <summary>Handles the reset sub command.</summary> @@ -422,25 +381,25 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other /// <param name="args">The command arguments.</param> 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<KeyValuePair<string, PerformanceCounter>> 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); - } - - /// <summary>Parses a command string and returns the associated command.</summary> - /// <param name="commandString">The command string</param> - /// <returns>The parsed command.</returns> - 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); } - /// <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> @@ -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"); } /// <summary>Shows detailed help for a specific sub command.</summary> - /// <param name="monitor">The output monitor</param> - /// <param name="subCommand">The sub command</param> - private void OutputHelp(IMonitor monitor, SubCommand subCommand) + /// <param name="monitor">The output monitor.</param> + /// <param name="subcommand">The subcommand.</param> + 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 <collection> <source>"); - sb.AppendLine(" pc detail <collection> <threshold>"); - sb.AppendLine(); - sb.AppendLine("Displays details for a specific collection."); - sb.AppendLine(); - sb.AppendLine("Arguments:"); - sb.AppendLine(" <collection> Required. The full or partial name of the collection to display."); - sb.AppendLine(" <source> Optional. The full or partial name of the source."); - sb.AppendLine(" <threshold> 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 <collection> <source>"); + report.AppendLine(" pc detail <collection> <threshold>"); + report.AppendLine(); + report.AppendLine("Displays details for a specific collection."); + report.AppendLine(); + report.AppendLine("Arguments:"); + report.AppendLine(" <collection> Required. The full or partial name of the collection to display."); + report.AppendLine(" <source> Optional. The full or partial name of the source."); + 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("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 <mode|name> <threshold>"); - sb.AppendLine(); - sb.AppendLine("Displays the performance counter summary."); - sb.AppendLine(); - sb.AppendLine("Arguments:"); - sb.AppendLine(" <mode> 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(" <name> Optional. Only shows performance counter collections matching the given name"); - sb.AppendLine(" <threshold> 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 <mode|name> <threshold>"); + report.AppendLine(); + report.AppendLine("Displays the performance counter summary."); + report.AppendLine(); + report.AppendLine("Arguments:"); + report.AppendLine(" <mode> 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(" <name> Optional. Only shows performance counter collections matching the given name"); + report.AppendLine(" <threshold> 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 <mode>"); - sb.AppendLine("Usage: pc trigger collection <collectionName> <threshold>"); - sb.AppendLine("Usage: pc trigger collection <collectionName> <threshold> <sourceName>"); - sb.AppendLine(); - sb.AppendLine("Manages alert triggers."); - sb.AppendLine(); - sb.AppendLine("Arguments:"); - sb.AppendLine(" <mode> 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(" <collectionName> 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(" <sourceName> Optional. Specifies the name of a specific source. Must be an exact match."); - sb.AppendLine(); - sb.AppendLine(" <threshold> 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 <mode>"); + report.AppendLine("Usage: pc trigger collection <collectionName> <threshold>"); + report.AppendLine("Usage: pc 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("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 <type> <name>"); - sb.AppendLine(); - sb.AppendLine("Resets performance counters."); - sb.AppendLine(); - sb.AppendLine("Arguments:"); - sb.AppendLine(" <type> 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(" <name> Required if a <type> 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 <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("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); } /// <summary>Get the command description.</summary> 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 <command> <action>"); - 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 <command>', 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 <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 'pc help <command>', 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 + *********/ + /// <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 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 /// <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> - /// <param name="rightAlign">True to right-align the data, false for left-align. Default false.</param> + /// <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 @@ -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 @@ </None> </ItemGroup> - <ItemGroup> - <PackageReference Include="System.ValueTuple" Version="4.5.0" /> - </ItemGroup> - <Import Project="..\SMAPI.Internal\SMAPI.Internal.projitems" Label="Shared" /> <Import Project="..\..\build\common.targets" /> 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 /// <summary>The URL of the SMAPI home page.</summary> internal const string HomePageUrl = "https://smapi.io"; - /// <summary>The URL of the SMAPI home page.</summary> + /// <summary>The default performance counter name for unknown event handlers.</summary> internal const string GamePerformanceCounterName = "<StardewValley>"; /// <summary>The absolute path to the folder containing SMAPI's internal files.</summary> @@ -103,6 +103,7 @@ namespace StardewModdingAPI /// <summary>The language code for non-translated mod assets.</summary> 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 /// <summary>Construct an instance.</summary> /// <param name="monitor">Writes messages to the log.</param> /// <param name="modRegistry">The mod registry with which to identify mods.</param> - /// <param name="performanceCounterManager">The performance counter manager.</param> - public EventManager(IMonitor monitor, ModRegistry modRegistry, PerformanceCounterManager performanceCounterManager) + /// <param name="performanceMonitor">Tracks performance metrics.</param> + public EventManager(IMonitor monitor, ModRegistry modRegistry, PerformanceMonitor performanceMonitor) { // create shortcut initializers - ManagedEvent<TEventArgs> ManageEventOf<TEventArgs>(string typeName, string eventName) => new ManagedEvent<TEventArgs>($"{typeName}.{eventName}", monitor, modRegistry, performanceCounterManager); + ManagedEvent<TEventArgs> ManageEventOf<TEventArgs>(string typeName, string eventName, bool isPerformanceCritical = false) + { + return new ManagedEvent<TEventArgs>($"{typeName}.{eventName}", monitor, modRegistry, performanceMonitor, isPerformanceCritical); + } // init events (new) this.MenuChanged = ManageEventOf<MenuChangedEventArgs>(nameof(IModEvents.Display), nameof(IDisplayEvents.MenuChanged)); - this.Rendering = ManageEventOf<RenderingEventArgs>(nameof(IModEvents.Display), nameof(IDisplayEvents.Rendering)); - this.Rendered = ManageEventOf<RenderedEventArgs>(nameof(IModEvents.Display), nameof(IDisplayEvents.Rendered)); - this.RenderingWorld = ManageEventOf<RenderingWorldEventArgs>(nameof(IModEvents.Display), nameof(IDisplayEvents.RenderingWorld)); - this.RenderedWorld = ManageEventOf<RenderedWorldEventArgs>(nameof(IModEvents.Display), nameof(IDisplayEvents.RenderedWorld)); - this.RenderingActiveMenu = ManageEventOf<RenderingActiveMenuEventArgs>(nameof(IModEvents.Display), nameof(IDisplayEvents.RenderingActiveMenu)); - this.RenderedActiveMenu = ManageEventOf<RenderedActiveMenuEventArgs>(nameof(IModEvents.Display), nameof(IDisplayEvents.RenderedActiveMenu)); - this.RenderingHud = ManageEventOf<RenderingHudEventArgs>(nameof(IModEvents.Display), nameof(IDisplayEvents.RenderingHud)); - this.RenderedHud = ManageEventOf<RenderedHudEventArgs>(nameof(IModEvents.Display), nameof(IDisplayEvents.RenderedHud)); + this.Rendering = ManageEventOf<RenderingEventArgs>(nameof(IModEvents.Display), nameof(IDisplayEvents.Rendering), isPerformanceCritical: true); + this.Rendered = ManageEventOf<RenderedEventArgs>(nameof(IModEvents.Display), nameof(IDisplayEvents.Rendered), isPerformanceCritical: true); + this.RenderingWorld = ManageEventOf<RenderingWorldEventArgs>(nameof(IModEvents.Display), nameof(IDisplayEvents.RenderingWorld), isPerformanceCritical: true); + this.RenderedWorld = ManageEventOf<RenderedWorldEventArgs>(nameof(IModEvents.Display), nameof(IDisplayEvents.RenderedWorld), isPerformanceCritical: true); + this.RenderingActiveMenu = ManageEventOf<RenderingActiveMenuEventArgs>(nameof(IModEvents.Display), nameof(IDisplayEvents.RenderingActiveMenu), isPerformanceCritical: true); + this.RenderedActiveMenu = ManageEventOf<RenderedActiveMenuEventArgs>(nameof(IModEvents.Display), nameof(IDisplayEvents.RenderedActiveMenu), isPerformanceCritical: true); + this.RenderingHud = ManageEventOf<RenderingHudEventArgs>(nameof(IModEvents.Display), nameof(IDisplayEvents.RenderingHud), isPerformanceCritical: true); + this.RenderedHud = ManageEventOf<RenderedHudEventArgs>(nameof(IModEvents.Display), nameof(IDisplayEvents.RenderedHud), isPerformanceCritical: true); this.WindowResized = ManageEventOf<WindowResizedEventArgs>(nameof(IModEvents.Display), nameof(IDisplayEvents.WindowResized)); this.GameLaunched = ManageEventOf<GameLaunchedEventArgs>(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.GameLaunched)); - this.UpdateTicking = ManageEventOf<UpdateTickingEventArgs>(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.UpdateTicking)); - this.UpdateTicked = ManageEventOf<UpdateTickedEventArgs>(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.UpdateTicked)); - this.OneSecondUpdateTicking = ManageEventOf<OneSecondUpdateTickingEventArgs>(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.OneSecondUpdateTicking)); - this.OneSecondUpdateTicked = ManageEventOf<OneSecondUpdateTickedEventArgs>(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.OneSecondUpdateTicked)); + this.UpdateTicking = ManageEventOf<UpdateTickingEventArgs>(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.UpdateTicking), isPerformanceCritical: true); + this.UpdateTicked = ManageEventOf<UpdateTickedEventArgs>(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.UpdateTicked), isPerformanceCritical: true); + this.OneSecondUpdateTicking = ManageEventOf<OneSecondUpdateTickingEventArgs>(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.OneSecondUpdateTicking), isPerformanceCritical: true); + this.OneSecondUpdateTicked = ManageEventOf<OneSecondUpdateTickedEventArgs>(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.OneSecondUpdateTicked), isPerformanceCritical: true); this.SaveCreating = ManageEventOf<SaveCreatingEventArgs>(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.SaveCreating)); this.SaveCreated = ManageEventOf<SaveCreatedEventArgs>(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.SaveCreated)); this.Saving = ManageEventOf<SavingEventArgs>(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.Saving)); @@ -209,7 +214,7 @@ namespace StardewModdingAPI.Framework.Events this.ButtonPressed = ManageEventOf<ButtonPressedEventArgs>(nameof(IModEvents.Input), nameof(IInputEvents.ButtonPressed)); this.ButtonReleased = ManageEventOf<ButtonReleasedEventArgs>(nameof(IModEvents.Input), nameof(IInputEvents.ButtonReleased)); - this.CursorMoved = ManageEventOf<CursorMovedEventArgs>(nameof(IModEvents.Input), nameof(IInputEvents.CursorMoved)); + this.CursorMoved = ManageEventOf<CursorMovedEventArgs>(nameof(IModEvents.Input), nameof(IInputEvents.CursorMoved), isPerformanceCritical: true); this.MouseWheelScrolled = ManageEventOf<MouseWheelScrolledEventArgs>(nameof(IModEvents.Input), nameof(IInputEvents.MouseWheelScrolled)); this.PeerContextReceived = ManageEventOf<PeerContextReceivedEventArgs>(nameof(IModEvents.Multiplayer), nameof(IMultiplayerEvents.PeerContextReceived)); @@ -230,8 +235,15 @@ namespace StardewModdingAPI.Framework.Events this.TerrainFeatureListChanged = ManageEventOf<TerrainFeatureListChangedEventArgs>(nameof(IModEvents.World), nameof(IWorldEvents.TerrainFeatureListChanged)); this.LoadStageChanged = ManageEventOf<LoadStageChangedEventArgs>(nameof(IModEvents.Specialized), nameof(ISpecializedEvents.LoadStageChanged)); - this.UnvalidatedUpdateTicking = ManageEventOf<UnvalidatedUpdateTickingEventArgs>(nameof(IModEvents.Specialized), nameof(ISpecializedEvents.UnvalidatedUpdateTicking)); - this.UnvalidatedUpdateTicked = ManageEventOf<UnvalidatedUpdateTickedEventArgs>(nameof(IModEvents.Specialized), nameof(ISpecializedEvents.UnvalidatedUpdateTicked)); + this.UnvalidatedUpdateTicking = ManageEventOf<UnvalidatedUpdateTickingEventArgs>(nameof(IModEvents.Specialized), nameof(ISpecializedEvents.UnvalidatedUpdateTicking), isPerformanceCritical: true); + this.UnvalidatedUpdateTicked = ManageEventOf<UnvalidatedUpdateTickedEventArgs>(nameof(IModEvents.Specialized), nameof(ISpecializedEvents.UnvalidatedUpdateTicked), isPerformanceCritical: true); + } + + /// <summary>Get all managed events.</summary> + public IEnumerable<IManagedEvent> 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 { + /// <summary>Metadata for an event raised by SMAPI.</summary> internal interface IManagedEvent { - string GetName(); + /********* + ** Accessors + *********/ + /// <summary>A human-readable name for the event.</summary> + string EventName { get; } + + /// <summary>Whether the event is typically called at least once per second.</summary> + 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 { /// <summary>An event wrapper which intercepts and logs errors in handler code.</summary> /// <typeparam name="TEventArgs">The event arguments type.</typeparam> - internal class ManagedEvent<TEventArgs>: IManagedEvent + internal class ManagedEvent<TEventArgs> : IManagedEvent { /********* ** Fields @@ -15,9 +15,6 @@ namespace StardewModdingAPI.Framework.Events /// <summary>The underlying event.</summary> private event EventHandler<TEventArgs> Event; - /// <summary>A human-readable name for the event.</summary> - private readonly string EventName; - /// <summary>Writes messages to the log.</summary> private readonly IMonitor Monitor; @@ -30,8 +27,19 @@ namespace StardewModdingAPI.Framework.Events /// <summary>The cached invocation list.</summary> private EventHandler<TEventArgs>[] CachedInvocationList; - /// <summary>The performance counter manager.</summary> - private readonly PerformanceCounterManager PerformanceCounterManager; + /// <summary>Tracks performance metrics.</summary> + private readonly PerformanceMonitor PerformanceMonitor; + + + /********* + ** Accessors + *********/ + /// <summary>A human-readable name for the event.</summary> + public string EventName { get; } + + /// <summary>Whether the event is typically called at least once per second.</summary> + public bool IsPerformanceCritical { get; } + /********* ** Public methods @@ -40,19 +48,15 @@ namespace StardewModdingAPI.Framework.Events /// <param name="eventName">A human-readable name for the event.</param> /// <param name="monitor">Writes messages to the log.</param> /// <param name="modRegistry">The mod registry with which to identify mods.</param> - /// <param name="performanceCounterManager">The performance counter manager</param> - public ManagedEvent(string eventName, IMonitor monitor, ModRegistry modRegistry, PerformanceCounterManager performanceCounterManager) + /// <param name="performanceMonitor">Tracks performance metrics.</param> + /// <param name="isPerformanceCritical">Whether the event is typically called at least once per second.</param> + 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; - } - - /// <summary>Gets the event name.</summary> - public string GetName() - { - return this.EventName; + this.PerformanceMonitor = performanceMonitor; + this.IsPerformanceCritical = isPerformanceCritical; } /// <summary>Get whether anything is listening to the event.</summary> @@ -93,22 +97,20 @@ namespace StardewModdingAPI.Framework.Events return; - this.PerformanceCounterManager.BeginTrackInvocation(this.EventName); - - foreach (EventHandler<TEventArgs> 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<TEventArgs> 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); + }); } /// <summary>Raise the event and notify all handlers.</summary> @@ -139,18 +141,17 @@ namespace StardewModdingAPI.Framework.Events /********* ** Private methods *********/ - + /// <summary>Get the mod name for a given event handler to display in performance monitoring reports.</summary> + /// <param name="handler">The event handler.</param> private string GetModNameForPerformanceCounters(EventHandler<TEventArgs> 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; } /// <summary>Track an event handler.</summary> 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 /// <summary>The context for an alert.</summary> internal struct AlertContext { + /********* + ** Accessors + *********/ /// <summary>The source which triggered the alert.</summary> - public readonly string Source; + public string Source { get; } /// <summary>The elapsed milliseconds.</summary> - public readonly double Elapsed; + public double Elapsed { get; } - /// <summary>Creates a new alert context.</summary> + + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> /// <param name="source">The source which triggered the alert.</param> /// <param name="elapsed">The elapsed milliseconds.</param> public AlertContext(string source, double elapsed) @@ -18,6 +25,7 @@ namespace StardewModdingAPI.Framework.PerformanceCounter this.Elapsed = elapsed; } + /// <summary>Get a human-readable text form of this instance.</summary> 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 { /// <summary>A single alert entry.</summary> internal struct AlertEntry { + /********* + ** Accessors + *********/ /// <summary>The collection in which the alert occurred.</summary> - public readonly PerformanceCounterCollection Collection; + public PerformanceCounterCollection Collection { get; } /// <summary>The actual execution time in milliseconds.</summary> - public readonly double ExecutionTimeMilliseconds; + public double ExecutionTimeMilliseconds { get; } + + /// <summary>The configured alert threshold in milliseconds.</summary> + public double ThresholdMilliseconds { get; } - /// <summary>The configured alert threshold. </summary> - public readonly double ThresholdMilliseconds; + /// <summary>The sources involved in exceeding the threshold.</summary> + public AlertContext[] Context { get; } - /// <summary>The context list, which records all sources involved in exceeding the threshold.</summary> - public readonly List<AlertContext> Context; - /// <summary>Creates a new alert entry.</summary> - /// <param name="collection">The source collection in which the alert occurred.</param> + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + /// <param name="collection">The collection in which the alert occurred.</param> /// <param name="executionTimeMilliseconds">The actual execution time in milliseconds.</param> - /// <param name="thresholdMilliseconds">The configured threshold in milliseconds.</param> - /// <param name="context">A list of AlertContext to record which sources were involved</param> - public AlertEntry(PerformanceCounterCollection collection, double executionTimeMilliseconds, double thresholdMilliseconds, List<AlertContext> context) + /// <param name="thresholdMilliseconds">The configured alert threshold in milliseconds.</param> + /// <param name="context">The sources involved in exceeding the threshold.</param> + 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 -{ - /// <summary>Represents a performance counter collection specific to game events.</summary> - internal class EventPerformanceCounterCollection: PerformanceCounterCollection - { - /// <summary>Creates a new event performance counter collection.</summary> - /// <param name="manager">The performance counter manager.</param> - /// <param name="event">The ManagedEvent.</param> - /// <param name="isImportant">If the event is flagged as important.</param> - 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 { + /// <summary>A peak invocation time.</summary> internal struct PeakEntry { + /********* + ** Accessors + *********/ /// <summary>The actual execution time in milliseconds.</summary> - public readonly double ExecutionTimeMilliseconds; + public double ExecutionTimeMilliseconds { get; } - /// <summary>The DateTime when the entry occured.</summary> - public DateTime EventTime; + /// <summary>When the entry occurred.</summary> + public DateTime EventTime { get; } - /// <summary>The context list, which records all sources involved in exceeding the threshold.</summary> - public readonly List<AlertContext> Context; + /// <summary>The sources involved in exceeding the threshold.</summary> + public AlertContext[] Context { get; } - public PeakEntry(double executionTimeMilliseconds, DateTime eventTime, List<AlertContext> context) + + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + /// <param name="executionTimeMilliseconds">The actual execution time in milliseconds.</param> + /// <param name="eventTime">When the entry occurred.</param> + /// <param name="context">The sources involved in exceeding the threshold.</param> + 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 { + /// <summary>Tracks metadata about a particular code event.</summary> internal class PerformanceCounter { + /********* + ** Fields + *********/ /// <summary>The size of the ring buffer.</summary> - private const int MAX_ENTRIES = 16384; + private readonly int MaxEntries = 16384; /// <summary>The collection to which this performance counter belongs.</summary> private readonly PerformanceCounterCollection ParentCollection; - /// <summary>The circular buffer which stores all performance counter entries</summary> - private readonly CircularBuffer<PerformanceCounterEntry> _counter; + /// <summary>The performance counter entries.</summary> + private readonly Stack<PerformanceCounterEntry> Entries; - /// <summary>The peak execution time</summary> + /// <summary>The entry with the highest execution time.</summary> private PerformanceCounterEntry? PeakPerformanceCounterEntry; + + /********* + ** Accessors + *********/ /// <summary>The name of the source.</summary> public string Source { get; } @@ -27,118 +36,90 @@ namespace StardewModdingAPI.Framework.PerformanceCounter /// <summary>If alerting is enabled or not</summary> public bool EnableAlerts { get; set; } + + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + /// <param name="parentCollection">The collection to which this performance counter belongs.</param> + /// <param name="source">The name of the source.</param> public PerformanceCounter(PerformanceCounterCollection parentCollection, string source) { this.ParentCollection = parentCollection; this.Source = source; - this._counter = new CircularBuffer<PerformanceCounterEntry>(PerformanceCounter.MAX_ENTRIES); + this.Entries = new Stack<PerformanceCounterEntry>(this.MaxEntries); } - /// <summary>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.</summary> + /// <summary>Add a performance counter entry to the list, update monitoring, and raise alerts if needed.</summary> /// <param name="entry">The entry to add.</param> 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)); } - /// <summary>Clears all performance counter entries and resets the peak entry.</summary> + /// <summary>Clear all performance counter entries and monitoring.</summary> public void Reset() { - this._counter.Clear(); + this.Entries.Clear(); this.PeakPerformanceCounterEntry = null; } - /// <summary>Returns the peak entry.</summary> - /// <returns>The peak entry.</returns> + /// <summary>Get the peak entry.</summary> public PerformanceCounterEntry? GetPeak() { return this.PeakPerformanceCounterEntry; } - /// <summary>Returns the peak entry.</summary> - /// <returns>The peak entry.</returns> - public PerformanceCounterEntry? GetPeak(TimeSpan range, DateTime? relativeTo = null) + /// <summary>Get the entry with the highest execution time.</summary> + /// <param name="range">The time range to search.</param> + /// <param name="endTime">The end time for the <paramref name="range"/>, or null for the current time.</param> + 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(); } - /// <summary>Resets the peak entry.</summary> - public void ResetPeak() - { - this.PeakPerformanceCounterEntry = null; - } - - /// <summary>Returns the last entry added to the list.</summary> - /// <returns>The last entry</returns> + /// <summary>Get the last entry added to the list.</summary> public PerformanceCounterEntry? GetLastEntry() { - if (this._counter.IsEmpty) + if (this.Entries.Count == 0) return null; - return this._counter.PeekLast(); + return this.Entries.Peek(); } - /// <summary>Returns the average execution time of all entries.</summary> - /// <returns>The average execution time in milliseconds.</returns> - public double GetAverage() + /// <summary>Get the average over a given time span.</summary> + /// <param name="range">The time range to search.</param> + /// <param name="endTime">The end time for the <paramref name="range"/>, or null for the current time.</param> + public double GetAverage(TimeSpan range, DateTime? endTime = null) { - if (this._counter.IsEmpty) - return 0; - - return this._counter.Average(p => p.ElapsedMilliseconds); - } - - /// <summary>Returns the average over a given time span.</summary> - /// <param name="range">The time range to retrieve.</param> - /// <param name="relativeTo">The DateTime from which to start the average. Defaults to DateTime.UtcNow if null</param> - /// <returns>The average execution time in milliseconds.</returns> - /// <remarks> - /// 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. - /// </remarks> - 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 { - /// <summary>The size of the ring buffer.</summary> - private const int MAX_ENTRIES = 16384; + /********* + ** Fields + *********/ + /// <summary>The number of peak invocations to keep.</summary> + private readonly int MaxEntries = 16384; - /// <summary>The list of triggered performance counters.</summary> + /// <summary>The sources involved in exceeding alert thresholds.</summary> private readonly List<AlertContext> TriggeredPerformanceCounters = new List<AlertContext>(); /// <summary>The stopwatch used to track the invocation time.</summary> private readonly Stopwatch InvocationStopwatch = new Stopwatch(); /// <summary>The performance counter manager.</summary> - private readonly PerformanceCounterManager PerformanceCounterManager; + private readonly PerformanceMonitor PerformanceMonitor; - /// <summary>Holds the time to calculate the average calls per second.</summary> + /// <summary>The time to calculate average calls per second.</summary> private DateTime CallsPerSecondStart = DateTime.UtcNow; - /// <summary>The number of invocations of this collection.</summary> + /// <summary>The number of invocations.</summary> private long CallCount; - /// <summary>The circular buffer which stores all peak invocations</summary> - private readonly CircularBuffer<PeakEntry> PeakInvocations; + /// <summary>The peak invocations.</summary> + private readonly Stack<PeakEntry> PeakInvocations; + + /********* + ** Accessors + *********/ /// <summary>The associated performance counters.</summary> public IDictionary<string, PerformanceCounter> PerformanceCounters { get; } = new Dictionary<string, PerformanceCounter>(); /// <summary>The name of this collection.</summary> public string Name { get; } - /// <summary>Flag if this collection is important (used for the console summary command).</summary> - public bool IsImportant { get; } + /// <summary>Whether the source is typically invoked at least once per second.</summary> + public bool IsPerformanceCritical { get; } /// <summary>The alert threshold in milliseconds.</summary> public double AlertThresholdMilliseconds { get; set; } - /// <summary>If alerting is enabled or not</summary> + /// <summary>Whether alerts are enabled.</summary> public bool EnableAlerts { get; set; } - public PerformanceCounterCollection(PerformanceCounterManager performanceCounterManager, string name, bool isImportant) + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + /// <param name="performanceMonitor">The performance counter manager.</param> + /// <param name="name">The name of this collection.</param> + /// <param name="isPerformanceCritical">Whether the source is typically invoked at least once per second.</param> + public PerformanceCounterCollection(PerformanceMonitor performanceMonitor, string name, bool isPerformanceCritical = false) { - this.PeakInvocations = new CircularBuffer<PeakEntry>(PerformanceCounterCollection.MAX_ENTRIES); + this.PeakInvocations = new Stack<PeakEntry>(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<PeakEntry>(PerformanceCounterCollection.MAX_ENTRIES); - this.PerformanceCounterManager = performanceCounterManager; - this.Name = name; - } - - /// <summary>Tracks a single invocation for a named source.</summary> + /// <summary>Track a single invocation for a named source.</summary> /// <param name="source">The name of the source.</param> /// <param name="entry">The entry.</param> 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)); } - /// <summary>Returns the average execution time for all non-game internal sources.</summary> - /// <returns>The average execution time in milliseconds</returns> - public double GetModsAverageExecutionTime() - { - return this.PerformanceCounters.Where(p => - p.Key != Constants.GamePerformanceCounterName).Sum(p => p.Value.GetAverage()); - } - - /// <summary>Returns the average execution time for all non-game internal sources.</summary> + /// <summary>Get the average execution time for all non-game internal sources in milliseconds.</summary> /// <param name="interval">The interval for which to get the average, relative to now</param> - /// <returns>The average execution time in milliseconds</returns> public double GetModsAverageExecutionTime(TimeSpan interval) { - return this.PerformanceCounters.Where(p => - p.Key != Constants.GamePerformanceCounterName).Sum(p => p.Value.GetAverage(interval)); - } - - /// <summary>Returns the overall average execution time.</summary> - /// <returns>The average execution time in milliseconds</returns> - 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)); } - /// <summary>Returns the overall average execution time.</summary> + /// <summary>Get the overall average execution time in milliseconds.</summary> /// <param name="interval">The interval for which to get the average, relative to now</param> - /// <returns>The average execution time in milliseconds</returns> public double GetAverageExecutionTime(TimeSpan interval) { - return this.PerformanceCounters.Sum(p => p.Value.GetAverage(interval)); + return this.PerformanceCounters + .Sum(entry => entry.Value.GetAverage(interval)); } - /// <summary>Returns the average execution time for game-internal sources.</summary> - /// <returns>The average execution time in milliseconds</returns> - public double GetGameAverageExecutionTime() - { - if (this.PerformanceCounters.TryGetValue(Constants.GamePerformanceCounterName, out PerformanceCounter gameExecTime)) - return gameExecTime.GetAverage(); - - return 0; - } - - /// <summary>Returns the average execution time for game-internal sources.</summary> - /// <returns>The average execution time in milliseconds</returns> + /// <summary>Get the average execution time for game-internal sources in milliseconds.</summary> 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; } - /// <summary>Returns the peak execution time</summary> - /// <param name="interval">The interval for which to get the peak, relative to <paramref name="relativeTo"/></param> - /// <param name="relativeTo">The DateTime which the <paramref name="interval"/> is relative to, or DateTime.Now if not given</param> - /// <returns>The peak execution time</returns> - public double GetPeakExecutionTime(TimeSpan range, DateTime? relativeTo = null) + /// <summary>Get the peak execution time in milliseconds.</summary> + /// <param name="range">The time range to search.</param> + /// <param name="endTime">The end time for the <paramref name="range"/>, or null for the current time.</param> + 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(); } - /// <summary>Begins tracking the invocation of this collection.</summary> + /// <summary>Start tracking the invocation of this collection.</summary> public void BeginTrackInvocation() { this.TriggeredPerformanceCounters.Clear(); @@ -158,60 +134,58 @@ namespace StardewModdingAPI.Framework.PerformanceCounter this.CallCount++; } - /// <summary>Ends tracking the invocation of this collection. Also records an alert if alerting is enabled - /// and the invocation time exceeds the threshold.</summary> + /// <summary>End tracking the invocation of this collection, and raise an alert if needed.</summary> 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()); } - /// <summary>Adds an alert.</summary> + /// <summary>Add an alert.</summary> /// <param name="executionTimeMilliseconds">The execution time in milliseconds.</param> /// <param name="thresholdMilliseconds">The configured threshold.</param> - /// <param name="alerts">The list of alert contexts.</param> - public void AddAlert(double executionTimeMilliseconds, double thresholdMilliseconds, List<AlertContext> alerts) + /// <param name="alerts">The sources involved in exceeding the threshold.</param> + 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) + ); } - /// <summary>Adds an alert for a single AlertContext</summary> + /// <summary>Add an alert.</summary> /// <param name="executionTimeMilliseconds">The execution time in milliseconds.</param> /// <param name="thresholdMilliseconds">The configured threshold.</param> - /// <param name="alert">The context</param> + /// <param name="alert">The source involved in exceeding the threshold.</param> public void AddAlert(double executionTimeMilliseconds, double thresholdMilliseconds, AlertContext alert) { - this.AddAlert(executionTimeMilliseconds, thresholdMilliseconds, new List<AlertContext>() {alert}); + this.AddAlert(executionTimeMilliseconds, thresholdMilliseconds, new[] { alert }); } - /// <summary>Resets the calls per second counter.</summary> + /// <summary>Reset the calls per second counter.</summary> public void ResetCallsPerSecond() { this.CallCount = 0; this.CallsPerSecondStart = DateTime.UtcNow; } - /// <summary>Resets all performance counters in this collection.</summary> + /// <summary>Reset all performance counters in this collection.</summary> public void Reset() { this.PeakInvocations.Clear(); - foreach (var i in this.PerformanceCounters) - i.Value.Reset(); + foreach (var counter in this.PerformanceCounters) + counter.Value.Reset(); } - /// <summary>Resets the performance counter for a specific source.</summary> - /// <param name="source">The source name</param> + /// <summary>Reset the performance counter for a specific source.</summary> + /// <param name="source">The source name.</param> public void ResetSource(string source) { foreach (var i in this.PerformanceCounters) @@ -219,15 +193,13 @@ namespace StardewModdingAPI.Framework.PerformanceCounter i.Value.Reset(); } - /// <summary>Returns the average calls per second.</summary> - /// <returns>The average calls per second.</returns> + /// <summary>Get the average calls per second.</summary> 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 { - /// <summary>A single performance counter entry. Records the DateTime of the event and the elapsed millisecond.</summary> + /// <summary>A single performance counter entry.</summary> internal struct PerformanceCounterEntry { - /// <summary>The DateTime when the entry occured.</summary> - public DateTime EventTime; + /********* + ** Accessors + *********/ + /// <summary>When the entry occurred.</summary> + public DateTime EventTime { get; } - /// <summary>The elapsed milliseconds</summary> - public double ElapsedMilliseconds; + /// <summary>The elapsed milliseconds.</summary> + public double ElapsedMilliseconds { get; } + + + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + /// <param name="eventTime">When the entry occurred.</param> + /// <param name="elapsedMilliseconds">The elapsed milliseconds.</param> + 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 + /// <summary>Tracks performance metrics.</summary> + internal class PerformanceMonitor { - public HashSet<PerformanceCounterCollection> PerformanceCounterCollections = new HashSet<PerformanceCounterCollection>(); - + /********* + ** Fields + *********/ /// <summary>The recorded alerts.</summary> - private readonly List<AlertEntry> Alerts = new List<AlertEntry>(); + private readonly IList<AlertEntry> Alerts = new List<AlertEntry>(); /// <summary>The monitor for output logging.</summary> private readonly IMonitor Monitor; @@ -20,62 +22,64 @@ namespace StardewModdingAPI.Framework.PerformanceCounter /// <summary>The invocation stopwatch.</summary> private readonly Stopwatch InvocationStopwatch = new Stopwatch(); - /// <summary>Specifies if alerts should be paused.</summary> + /// <summary>The underlying performance counter collections.</summary> + private readonly IDictionary<string, PerformanceCounterCollection> Collections = new Dictionary<string, PerformanceCounterCollection>(StringComparer.InvariantCultureIgnoreCase); + + + /********* + ** Accessors + *********/ + /// <summary>Whether alerts are paused.</summary> public bool PauseAlerts { get; set; } - /// <summary>Specifies if performance counter tracking should be enabled.</summary> + /// <summary>Whether performance counter tracking is enabled.</summary> public bool EnableTracking { get; set; } - /// <summary>Constructs a performance counter manager.</summary> + + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> /// <param name="monitor">The monitor for output logging.</param> - public PerformanceCounterManager(IMonitor monitor) + public PerformanceMonitor(IMonitor monitor) { this.Monitor = monitor; } - /// <summary>Resets all performance counters in all collections.</summary> + /// <summary>Reset all performance counters in all collections.</summary> 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(); - } } - /// <summary>Begins tracking the invocation for a collection.</summary> - /// <param name="collectionName">The collection name</param> - public void BeginTrackInvocation(string collectionName) + /// <summary>Track the invocation time for a collection.</summary> + /// <param name="collectionName">The name of the collection.</param> + /// <param name="action">The action to execute and track.</param> + public void Track(string collectionName, Action action) { if (!this.EnableTracking) { + action(); return; } - this.GetOrCreateCollectionByName(collectionName).BeginTrackInvocation(); - } - - /// <summary>Ends tracking the invocation for a collection.</summary> - /// <param name="collectionName"></param> - 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(); } - /// <summary>Tracks a single performance counter invocation in a specific collection.</summary> + /// <summary>Track a single performance counter invocation in a specific collection.</summary> /// <param name="collectionName">The name of the collection.</param> /// <param name="sourceName">The name of the source.</param> - /// <param name="action">The action to execute and track invocation time for.</param> + /// <param name="action">The action to execute and track.</param> 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)); } } - /// <summary>Gets a collection by name.</summary> - /// <param name="name">The name of the collection.</param> - /// <returns>The collection or null if none was found.</returns> - private PerformanceCounterCollection GetCollectionByName(string name) - { - return this.PerformanceCounterCollections.FirstOrDefault(collection => collection.Name == name); - } - - /// <summary>Gets a collection by name and creates it if it doesn't exist.</summary> - /// <param name="name">The name of the collection.</param> - /// <returns>The collection.</returns> - 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; - } - - /// <summary>Resets the performance counters for a specific collection.</summary> + /// <summary>Reset the performance counters for a specific collection.</summary> /// <param name="name">The collection name.</param> 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(); } } - /// <summary>Resets performance counters for a specific source.</summary> + /// <summary>Reset performance counters for a specific source.</summary> /// <param name="name">The name of the source.</param> public void ResetSource(string name) { - foreach (PerformanceCounterCollection performanceCounterCollection in this.PerformanceCounterCollections) + foreach (PerformanceCounterCollection performanceCounterCollection in this.Collections.Values) performanceCounterCollection.ResetSource(name); } /// <summary>Print any queued alerts.</summary> 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); } - /// <summary>Adds an alert to the queue.</summary> + /// <summary>Add an alert to the queue.</summary> /// <param name="entry">The alert to add.</param> public void AddAlert(AlertEntry entry) { @@ -175,68 +151,34 @@ namespace StardewModdingAPI.Framework.PerformanceCounter this.Alerts.Add(entry); } - /// <summary>Initialized the default performance counter collections.</summary> + /// <summary>Initialize the default performance counter collections.</summary> /// <param name="eventManager">The event manager.</param> public void InitializePerformanceCounterCollections(EventManager eventManager) { - this.PerformanceCounterCollections = new HashSet<PerformanceCounterCollection>() + foreach (IManagedEvent @event in eventManager.GetAllEvents()) + this.Collections[@event.EventName] = new PerformanceCounterCollection(this, @event.EventName, @event.IsPerformanceCritical); + } + + /// <summary>Get the underlying performance counters.</summary> + public IEnumerable<PerformanceCounterCollection> GetCollections() + { + return this.Collections.Values; + } + + + /********* + ** Public methods + *********/ + /// <summary>Get a collection by name and creates it if it doesn't exist.</summary> + /// <param name="name">The name of the collection.</param> + 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; } /// <summary>Manages performance counters.</summary> - /// <remarks>This is initialized after the game starts. This is accessed directly because it's not part of the normal class model.</remarks> - internal static PerformanceCounterManager PerformanceCounterManager { get; private set; } + /// <remarks>This is initialized after the game starts. This is non-private for use by Console Commands.</remarks> + 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 /// <summary>Manages deprecation warnings.</summary> private readonly DeprecationManager DeprecationManager; - private readonly PerformanceCounterManager PerformanceCounterManager; + /// <summary>Tracks performance metrics.</summary> + private readonly PerformanceMonitor PerformanceMonitor; /// <summary>The maximum number of consecutive attempts SMAPI should make to recover from a draw error.</summary> private readonly Countdown DrawCrashTimer = new Countdown(60); // 60 ticks = roughly one second @@ -155,12 +156,12 @@ namespace StardewModdingAPI.Framework /// <param name="jsonHelper">Encapsulates SMAPI's JSON file parsing.</param> /// <param name="modRegistry">Tracks the installed mods.</param> /// <param name="deprecationManager">Manages deprecation warnings.</param> - /// <param name="performanceCounterManager">Manages performance monitoring.</param> + /// <param name="performanceMonitor">Tracks performance metrics.</param> /// <param name="onGameInitialized">A callback to invoke after the game finishes initializing.</param> /// <param name="onGameExiting">A callback to invoke when the game exits.</param> /// <param name="cancellationToken">Propagates notification that SMAPI should exit.</param> /// <param name="logNetworkTraffic">Whether to log network traffic.</param> - 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 @@ <ApplicationIcon>icon.ico</ApplicationIcon> </PropertyGroup> - <PropertyGroup Condition=" '$(Configuration)' == 'Release' "> - <DefineConstants>SMAPI_FOR_WINDOWS</DefineConstants> - </PropertyGroup> - <ItemGroup> - <PackageReference Include="Cyotek.CircularBuffer" Version="1.0.2" /> <PackageReference Include="LargeAddressAware" Version="1.0.3" /> <PackageReference Include="Lib.Harmony" Version="1.2.0.1" /> <PackageReference Include="Mono.Cecil" Version="0.11.1" /> |