From c5be446701d4e24a03d8464e9b080ce74d158223 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 6 Jan 2021 00:29:39 -0500 Subject: rework vanilla tilesheet checking to avoid keeping a copy of the vanilla maps in memory --- docs/release-notes.md | 4 ++ src/SMAPI/Framework/Content/TilesheetReference.cs | 33 +++++++++++++++++ src/SMAPI/Framework/ContentCoordinator.cs | 43 ++++++++++++++++------ .../ContentManagers/GameContentManager.cs | 32 ++++++---------- 4 files changed, 80 insertions(+), 32 deletions(-) create mode 100644 src/SMAPI/Framework/Content/TilesheetReference.cs diff --git a/docs/release-notes.md b/docs/release-notes.md index 82709d3c..9e2caba0 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -7,6 +7,10 @@ * Migrated to Harmony 2.0 (see [_migrate to Harmony 2.0_](https://stardewvalleywiki.com/Modding:Migrate_to_Harmony_2.0) for more info). --> +## Upcoming release +* For players: + * Slightly reduced memory usage. + ## 3.8.2 Released 03 January 2021 for Stardew Valley 1.5.1 or later. diff --git a/src/SMAPI/Framework/Content/TilesheetReference.cs b/src/SMAPI/Framework/Content/TilesheetReference.cs new file mode 100644 index 00000000..2ea38430 --- /dev/null +++ b/src/SMAPI/Framework/Content/TilesheetReference.cs @@ -0,0 +1,33 @@ +namespace StardewModdingAPI.Framework.Content +{ + /// Basic metadata about a vanilla tilesheet. + internal class TilesheetReference + { + /********* + ** Accessors + *********/ + /// The tilesheet's index in the list. + public readonly int Index; + + /// The tilesheet's unique ID in the map. + public readonly string Id; + + /// The asset path for the tilesheet texture. + public readonly string ImageSource; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The tilesheet's index in the list. + /// The tilesheet's unique ID in the map. + /// The asset path for the tilesheet texture. + public TilesheetReference(int index, string id, string imageSource) + { + this.Index = index; + this.Id = id; + this.ImageSource = imageSource; + } + } +} diff --git a/src/SMAPI/Framework/ContentCoordinator.cs b/src/SMAPI/Framework/ContentCoordinator.cs index 3d5bb29d..27fb3dbb 100644 --- a/src/SMAPI/Framework/ContentCoordinator.cs +++ b/src/SMAPI/Framework/ContentCoordinator.cs @@ -54,6 +54,9 @@ namespace StardewModdingAPI.Framework /// The game may adds content managers in asynchronous threads (e.g. when populating the load screen). private readonly ReaderWriterLockSlim ContentManagerLock = new ReaderWriterLockSlim(); + /// A cache of ordered tilesheet IDs used by vanilla maps. + private readonly IDictionary VanillaTilesheets = new Dictionary(StringComparer.OrdinalIgnoreCase); + /// An unmodified content manager which doesn't intercept assets, used to compare asset data. private readonly LocalizedContentManager VanillaContentManager; @@ -293,21 +296,21 @@ namespace StardewModdingAPI.Framework }); } - /// Get a vanilla asset without interception. - /// The type of asset to load. + /// Get the tilesheet ID order used by the unmodified version of a map asset. /// The asset path relative to the loader root directory, not including the .xnb extension. - public bool TryLoadVanillaAsset(string assetName, out T asset) + public TilesheetReference[] GetVanillaTilesheetIds(string assetName) { - try + if (!this.VanillaTilesheets.TryGetValue(assetName, out TilesheetReference[] tilesheets)) { - asset = this.VanillaContentManager.Load(assetName); - return true; - } - catch - { - asset = default; - return false; + tilesheets = this.TryLoadVanillaAsset(assetName, out Map map) + ? map.TileSheets.Select((sheet, index) => new TilesheetReference(index, sheet.Id, sheet.ImageSource)).ToArray() + : null; + + this.VanillaTilesheets[assetName] = tilesheets; + this.VanillaContentManager.Unload(); } + + return tilesheets ?? new TilesheetReference[0]; } /// Dispose held resources. @@ -341,5 +344,23 @@ namespace StardewModdingAPI.Framework this.ContentManagers.Remove(contentManager) ); } + + /// Get a vanilla asset without interception. + /// The type of asset to load. + /// The asset path relative to the loader root directory, not including the .xnb extension. + /// The loaded asset data. + private bool TryLoadVanillaAsset(string assetName, out T asset) + { + try + { + asset = this.VanillaContentManager.Load(assetName); + return true; + } + catch + { + asset = default; + return false; + } + } } } diff --git a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs index 424d6ff3..540475f1 100644 --- a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs @@ -11,7 +11,6 @@ using StardewModdingAPI.Framework.Reflection; using StardewModdingAPI.Framework.Utilities; using StardewValley; using xTile; -using xTile.Tiles; namespace StardewModdingAPI.Framework.ContentManagers { @@ -398,14 +397,13 @@ namespace StardewModdingAPI.Framework.ContentManagers } // when replacing a map, the vanilla tilesheets must have the same order and IDs - if (data is Map loadedMap && this.Coordinator.TryLoadVanillaAsset(info.AssetName, out Map vanillaMap)) + if (data is Map loadedMap) { - for (int i = 0; i < vanillaMap.TileSheets.Count; i++) + TilesheetReference[] vanillaTilesheetRefs = this.Coordinator.GetVanillaTilesheetIds(info.AssetName); + foreach (TilesheetReference vanillaSheet in vanillaTilesheetRefs) { - // check for match - TileSheet vanillaSheet = vanillaMap.TileSheets[i]; - bool found = this.TryFindTilesheet(loadedMap, vanillaSheet.Id, out int loadedIndex, out TileSheet loadedSheet); - if (found && loadedIndex == i) + // skip if match + if (loadedMap.TileSheets.Count > vanillaSheet.Index && loadedMap.TileSheets[vanillaSheet.Index].Id == vanillaSheet.Id) continue; // handle mismatch @@ -414,9 +412,9 @@ namespace StardewModdingAPI.Framework.ContentManagers // This is temporary: mods shouldn't do this for any vanilla map, but these are the ones we know will crash. Showing a warning for others instead gives modders time to update their mods, while still simplifying troubleshooting. bool isFarmMap = info.AssetNameEquals("Maps/Farm") || info.AssetNameEquals("Maps/Farm_Combat") || info.AssetNameEquals("Maps/Farm_Fishing") || info.AssetNameEquals("Maps/Farm_Foraging") || info.AssetNameEquals("Maps/Farm_FourCorners") || info.AssetNameEquals("Maps/Farm_Island") || info.AssetNameEquals("Maps/Farm_Mining"); - - string reason = found - ? $"mod reordered the original tilesheets, which {(isFarmMap ? "would cause a crash" : "often causes crashes")}.\n\nTechnical details for mod author:\nExpected order [{string.Join(", ", vanillaMap.TileSheets.Select(p => $"'{p.ImageSource}' (id: {p.Id})"))}], but found tilesheet '{vanillaSheet.Id}' at index {loadedIndex} instead of {i}. Make sure custom tilesheet IDs are prefixed with 'z_' to avoid reordering tilesheets." + int loadedIndex = this.TryFindTilesheet(loadedMap, vanillaSheet.Id); + string reason = loadedIndex != -1 + ? $"mod reordered the original tilesheets, which {(isFarmMap ? "would cause a crash" : "often causes crashes")}.\n\nTechnical details for mod author:\nExpected order [{string.Join(", ", vanillaTilesheetRefs.Select(p => $"'{p.ImageSource}' (id: {p.Id})"))}], but found tilesheet '{vanillaSheet.Id}' at index {loadedIndex} instead of {vanillaSheet.Index}. Make sure custom tilesheet IDs are prefixed with 'z_' to avoid reordering tilesheets." : $"mod has no tilesheet with ID '{vanillaSheet.Id}'. Map replacements must keep the original tilesheets to avoid errors or crashes."; SCore.DeprecationManager.PlaceholderWarn("3.8.2", DeprecationLevel.PendingRemoval); @@ -436,23 +434,15 @@ namespace StardewModdingAPI.Framework.ContentManagers /// Find a map tilesheet by ID. /// The map whose tilesheets to search. /// The tilesheet ID to match. - /// The matched tilesheet index, if any. - /// The matched tilesheet, if any. - private bool TryFindTilesheet(Map map, string id, out int index, out TileSheet tilesheet) + private int TryFindTilesheet(Map map, string id) { for (int i = 0; i < map.TileSheets.Count; i++) { if (map.TileSheets[i].Id == id) - { - index = i; - tilesheet = map.TileSheets[i]; - return true; - } + return i; } - index = -1; - tilesheet = null; - return false; + return -1; } } } -- cgit From a179466e6b2800846bd425e2fe61de80d52d77bd Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 6 Jan 2021 00:44:24 -0500 Subject: remove experimental performance counters Unfortunately this impacted SMAPI's memory usage and the data was often misinterpreted by players. --- docs/release-notes.md | 5 +- .../Framework/Commands/ArgumentParser.cs | 50 -- .../Commands/Other/PerformanceCounterCommand.cs | 647 --------------------- .../Framework/Commands/TrainerCommand.cs | 5 +- src/SMAPI/Constants.cs | 3 - src/SMAPI/Framework/Events/EventManager.cs | 6 +- src/SMAPI/Framework/Events/ManagedEvent.cs | 44 +- .../PerformanceMonitoring/AlertContext.cs | 34 -- .../Framework/PerformanceMonitoring/AlertEntry.cs | 38 -- .../Framework/PerformanceMonitoring/PeakEntry.cs | 35 -- .../PerformanceMonitoring/PerformanceCounter.cs | 124 ---- .../PerformanceCounterCollection.cs | 205 ------- .../PerformanceCounterEntry.cs | 30 - .../PerformanceMonitoring/PerformanceMonitor.cs | 184 ------ src/SMAPI/Framework/SCore.cs | 10 +- src/SMAPI/Properties/AssemblyInfo.cs | 1 - 16 files changed, 21 insertions(+), 1400 deletions(-) delete mode 100644 src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/PerformanceCounterCommand.cs delete mode 100644 src/SMAPI/Framework/PerformanceMonitoring/AlertContext.cs delete mode 100644 src/SMAPI/Framework/PerformanceMonitoring/AlertEntry.cs delete mode 100644 src/SMAPI/Framework/PerformanceMonitoring/PeakEntry.cs delete mode 100644 src/SMAPI/Framework/PerformanceMonitoring/PerformanceCounter.cs delete mode 100644 src/SMAPI/Framework/PerformanceMonitoring/PerformanceCounterCollection.cs delete mode 100644 src/SMAPI/Framework/PerformanceMonitoring/PerformanceCounterEntry.cs delete mode 100644 src/SMAPI/Framework/PerformanceMonitoring/PerformanceMonitor.cs diff --git a/docs/release-notes.md b/docs/release-notes.md index 9e2caba0..4c548924 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -9,7 +9,10 @@ ## Upcoming release * For players: - * Slightly reduced memory usage. + * Reduced memory usage. + +* For the Console Commands mod: + * Removed experimental `performance` command. Unfortunately this impacted SMAPI's memory usage and the data was often misinterpreted. This may be replaced with more automatic performance alerts in a future version. ## 3.8.2 Released 03 January 2021 for Stardew Valley 1.5.1 or later. diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/ArgumentParser.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/ArgumentParser.cs index e84445d7..7e157c38 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/ArgumentParser.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/ArgumentParser.cs @@ -1,7 +1,6 @@ using System; using System.Collections; using System.Collections.Generic; -using System.Globalization; using System.Linq; namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands @@ -107,38 +106,6 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands return true; } - /// Try to read a decimal argument. - /// The argument index. - /// The argument name for error messages. - /// The parsed value. - /// Whether to show an error if the argument is missing. - /// The minimum value allowed. - /// The maximum value allowed. - public bool TryGetDecimal(int index, string name, out decimal value, bool required = true, decimal? min = null, decimal? max = null) - { - value = 0; - - // get argument - if (!this.TryGet(index, name, out string raw, required)) - return false; - - // parse - if (!decimal.TryParse(raw, NumberStyles.Number, CultureInfo.InvariantCulture, out value)) - { - this.LogDecimalFormatError(index, name, min, max); - return false; - } - - // validate - if ((min.HasValue && value < min) || (max.HasValue && value > max)) - { - this.LogDecimalFormatError(index, name, min, max); - return false; - } - - return true; - } - /// Returns an enumerator that iterates through the collection. /// An enumerator that can be used to iterate through the collection. public IEnumerator GetEnumerator() @@ -180,22 +147,5 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands else this.LogError($"Argument {index} ({name}) must be an integer."); } - - /// Print an error for an invalid decimal argument. - /// The argument index. - /// The argument name for error messages. - /// The minimum value allowed. - /// The maximum value allowed. - private void LogDecimalFormatError(int index, string name, decimal? min, decimal? max) - { - if (min.HasValue && max.HasValue) - this.LogError($"Argument {index} ({name}) must be a decimal between {min} and {max}."); - else if (min.HasValue) - this.LogError($"Argument {index} ({name}) must be a decimal and at least {min}."); - else if (max.HasValue) - this.LogError($"Argument {index} ({name}) must be a decimal and at most {max}."); - else - this.LogError($"Argument {index} ({name}) must be a decimal."); - } } } diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/PerformanceCounterCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/PerformanceCounterCommand.cs deleted file mode 100644 index 63851c9d..00000000 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/PerformanceCounterCommand.cs +++ /dev/null @@ -1,647 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using StardewModdingAPI.Framework; -using StardewModdingAPI.Framework.PerformanceMonitoring; - -namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other -{ - /// A set of commands which displays or configures performance monitoring. - internal class PerformanceCounterCommand : TrainerCommand - { - /********* - ** Fields - *********/ - /// The name of the command. - private const string CommandName = "performance"; - - /// The available commands. - private enum SubCommand - { - Summary, - Detail, - Reset, - Trigger, - Enable, - Disable, - Help - } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - public PerformanceCounterCommand() - : base(CommandName, PerformanceCounterCommand.GetDescription()) { } - - /// Handle the command. - /// Writes messages to the console and log file. - /// The command name. - /// The command arguments. - public override void Handle(IMonitor monitor, string command, ArgumentParser args) - { - // parse args - SubCommand subcommand = SubCommand.Summary; - { - if (args.TryGet(0, "command", out string subcommandStr, false) && !Enum.TryParse(subcommandStr, ignoreCase: true, out subcommand)) - { - this.LogUsageError(monitor, $"Unknown command {subcommandStr}"); - return; - } - } - - // handle - switch (subcommand) - { - case SubCommand.Summary: - this.HandleSummarySubCommand(monitor, args); - break; - - case SubCommand.Detail: - this.HandleDetailSubCommand(monitor, args); - break; - - case SubCommand.Reset: - this.HandleResetSubCommand(monitor, args); - break; - - case SubCommand.Trigger: - this.HandleTriggerSubCommand(monitor, args); - break; - - case SubCommand.Enable: - SCore.PerformanceMonitor.EnableTracking = true; - monitor.Log("Performance counter tracking is now enabled", LogLevel.Info); - break; - - case SubCommand.Disable: - SCore.PerformanceMonitor.EnableTracking = false; - monitor.Log("Performance counter tracking is now disabled", LogLevel.Info); - break; - - case SubCommand.Help: - this.OutputHelp(monitor, args.TryGet(1, "command", out _) ? subcommand : null as SubCommand?); - break; - - default: - this.LogUsageError(monitor, $"Unknown command {subcommand}"); - break; - } - } - - - /********* - ** Private methods - *********/ - /// Handles the summary sub command. - /// Writes messages to the console and log file. - /// The command arguments. - private void HandleSummarySubCommand(IMonitor monitor, ArgumentParser args) - { - if (!this.AssertEnabled(monitor)) - return; - - IEnumerable data = SCore.PerformanceMonitor.GetCollections(); - - double? threshold = null; - if (args.TryGetDecimal(1, "threshold", out decimal t, required: false)) - threshold = (double?)t; - - TimeSpan interval = TimeSpan.FromSeconds(60); - - StringBuilder report = new StringBuilder(); - report.AppendLine($"Summary over the last {interval.TotalSeconds} seconds:"); - report.AppendLine(this.GetTableString( - data: data, - header: new[] { "Collection", "Avg Calls/s", "Avg Exec Time (Game)", "Avg Exec Time (Mods)", "Avg Exec Time (Game+Mods)", "Peak Exec Time" }, - getRow: item => new[] - { - item.Name, - item.GetAverageCallsPerSecond().ToString(), - this.FormatMilliseconds(item.GetGameAverageExecutionTime(interval), threshold), - this.FormatMilliseconds(item.GetModsAverageExecutionTime(interval), threshold), - this.FormatMilliseconds(item.GetAverageExecutionTime(interval), threshold), - this.FormatMilliseconds(item.GetPeakExecutionTime(interval), threshold) - }, - true - )); - - monitor.Log(report.ToString(), LogLevel.Info); - } - - /// Handles the detail sub command. - /// Writes messages to the console and log file. - /// The command arguments. - private void HandleDetailSubCommand(IMonitor monitor, ArgumentParser args) - { - if (!this.AssertEnabled(monitor)) - return; - - // parse args - double thresholdMilliseconds = 0; - if (args.TryGetDecimal(1, "threshold", out decimal t, required: false)) - thresholdMilliseconds = (double)t; - - // get collections - var collections = SCore.PerformanceMonitor.GetCollections(); - - // render - TimeSpan averageInterval = TimeSpan.FromSeconds(60); - StringBuilder report = new StringBuilder($"Showing details for performance counters of {thresholdMilliseconds}+ milliseconds:\n\n"); - bool anyShown = false; - foreach (PerformanceCounterCollection collection in collections) - { - KeyValuePair[] data = collection.PerformanceCounters - .Where(p => p.Value.GetAverage(averageInterval) >= thresholdMilliseconds) - .ToArray(); - - if (data.Any()) - { - anyShown = true; - report.AppendLine($"{collection.Name}:"); - report.AppendLine(this.GetTableString( - data: data, - header: new[] { "Mod", $"Avg Exec Time (last {(int)averageInterval.TotalSeconds}s)", "Last Exec Time", "Peak Exec Time", $"Peak Exec Time (last {(int)averageInterval.TotalSeconds}s)" }, - getRow: item => new[] - { - item.Key, - this.FormatMilliseconds(item.Value.GetAverage(averageInterval), thresholdMilliseconds), - this.FormatMilliseconds(item.Value.GetLastEntry()?.ElapsedMilliseconds), - this.FormatMilliseconds(item.Value.GetPeak()?.ElapsedMilliseconds), - this.FormatMilliseconds(item.Value.GetPeak(averageInterval)?.ElapsedMilliseconds) - }, - true - )); - } - } - - if (!anyShown) - report.AppendLine("No performance counters found."); - - monitor.Log(report.ToString(), LogLevel.Info); - } - - /// Handles the trigger sub command. - /// Writes messages to the console and log file. - /// The command arguments. - private void HandleTriggerSubCommand(IMonitor monitor, ArgumentParser args) - { - if (!this.AssertEnabled(monitor)) - return; - - if (args.TryGet(1, "mode", out string mode, false)) - { - switch (mode) - { - case "list": - this.OutputAlertTriggers(monitor); - break; - - case "collection": - if (args.TryGet(2, "name", out string collectionName)) - { - if (args.TryGetDecimal(3, "threshold", out decimal threshold)) - { - if (!args.TryGet(4, "source", out string source, required: false)) - source = null; - this.ConfigureAlertTrigger(monitor, collectionName, source, threshold); - } - } - break; - - case "pause": - SCore.PerformanceMonitor.PauseAlerts = true; - monitor.Log("Alerts are now paused.", LogLevel.Info); - break; - - case "resume": - SCore.PerformanceMonitor.PauseAlerts = false; - monitor.Log("Alerts are now resumed.", LogLevel.Info); - break; - - case "dump": - this.OutputAlertTriggers(monitor, true); - break; - - case "clear": - this.ClearAlertTriggers(monitor); - break; - - default: - this.LogUsageError(monitor, $"Unknown mode {mode}. See '{CommandName} help trigger' for usage."); - break; - } - } - else - this.OutputAlertTriggers(monitor); - } - - /// Sets up an an alert trigger. - /// Writes messages to the console and log file. - /// The name of the collection. - /// The name of the source, or null for all sources. - /// The trigger threshold, or 0 to remove. - private void ConfigureAlertTrigger(IMonitor monitor, string collectionName, string sourceName, decimal threshold) - { - foreach (PerformanceCounterCollection collection in SCore.PerformanceMonitor.GetCollections()) - { - if (collection.Name.ToLowerInvariant().Equals(collectionName.ToLowerInvariant())) - { - if (sourceName == null) - { - if (threshold != 0) - { - collection.EnableAlerts = true; - collection.AlertThresholdMilliseconds = (double)threshold; - monitor.Log($"Set up alert triggering for '{collectionName}' with '{this.FormatMilliseconds((double?)threshold)}'", LogLevel.Info); - } - else - { - collection.EnableAlerts = false; - monitor.Log($"Cleared alert triggering for '{collection}'."); - } - - return; - } - else - { - foreach (var performanceCounter in collection.PerformanceCounters) - { - if (performanceCounter.Value.Source.ToLowerInvariant().Equals(sourceName.ToLowerInvariant())) - { - if (threshold != 0) - { - performanceCounter.Value.EnableAlerts = true; - performanceCounter.Value.AlertThresholdMilliseconds = (double)threshold; - monitor.Log($"Set up alert triggering for '{sourceName}' in collection '{collectionName}' with '{this.FormatMilliseconds((double?)threshold)}", LogLevel.Info); - } - else - performanceCounter.Value.EnableAlerts = false; - return; - } - } - - monitor.Log($"Could not find the source '{sourceName}' in collection '{collectionName}'", LogLevel.Warn); - return; - } - } - } - - monitor.Log($"Could not find the collection '{collectionName}'", LogLevel.Warn); - } - - - /// Clears alert triggering for all collections. - /// Writes messages to the console and log file. - private void ClearAlertTriggers(IMonitor monitor) - { - int clearedTriggers = 0; - foreach (PerformanceCounterCollection collection in SCore.PerformanceMonitor.GetCollections()) - { - if (collection.EnableAlerts) - { - collection.EnableAlerts = false; - clearedTriggers++; - } - - foreach (var performanceCounter in collection.PerformanceCounters) - { - if (performanceCounter.Value.EnableAlerts) - { - performanceCounter.Value.EnableAlerts = false; - clearedTriggers++; - } - } - - } - - monitor.Log($"Cleared {clearedTriggers} alert triggers.", LogLevel.Info); - } - - /// Lists all configured alert triggers. - /// Writes messages to the console and log file. - /// True to dump the triggers as commands. - private void OutputAlertTriggers(IMonitor monitor, bool asDump = false) - { - StringBuilder report = new StringBuilder(); - report.AppendLine("Configured triggers:"); - report.AppendLine(); - var collectionTriggers = new List(); - var sourceTriggers = new List(); - - foreach (PerformanceCounterCollection collection in SCore.PerformanceMonitor.GetCollections()) - { - if (collection.EnableAlerts) - collectionTriggers.Add(new CollectionTrigger(collection.Name, collection.AlertThresholdMilliseconds)); - - sourceTriggers.AddRange( - from counter in collection.PerformanceCounters - where counter.Value.EnableAlerts - select new SourceTrigger(collection.Name, counter.Value.Source, counter.Value.AlertThresholdMilliseconds) - ); - } - - if (collectionTriggers.Count > 0) - { - report.AppendLine("Collection Triggers:"); - report.AppendLine(); - - if (asDump) - { - foreach (var item in collectionTriggers) - report.AppendLine($"{CommandName} trigger {item.CollectionName} {item.Threshold}"); - } - else - { - report.AppendLine(this.GetTableString( - data: collectionTriggers, - header: new[] { "Collection", "Threshold" }, - getRow: item => new[] { item.CollectionName, this.FormatMilliseconds(item.Threshold) }, - true - )); - } - - report.AppendLine(); - } - else - report.AppendLine("No collection triggers."); - - if (sourceTriggers.Count > 0) - { - report.AppendLine("Source Triggers:"); - report.AppendLine(); - - if (asDump) - { - foreach (SourceTrigger item in sourceTriggers) - report.AppendLine($"{CommandName} trigger {item.CollectionName} {item.Threshold} {item.SourceName}"); - } - else - { - report.AppendLine(this.GetTableString( - data: sourceTriggers, - header: new[] { "Collection", "Source", "Threshold" }, - getRow: item => new[] { item.CollectionName, item.SourceName, this.FormatMilliseconds(item.Threshold) }, - true - )); - } - - report.AppendLine(); - } - else - report.AppendLine("No source triggers."); - - monitor.Log(report.ToString(), LogLevel.Info); - } - - /// Handles the reset sub command. - /// Writes messages to the console and log file. - /// The command arguments. - private void HandleResetSubCommand(IMonitor monitor, ArgumentParser args) - { - if (!this.AssertEnabled(monitor)) - return; - - if (args.TryGet(1, "type", out string type, false, new[] { "category", "source" })) - { - args.TryGet(2, "name", out string name); - - switch (type) - { - case "category": - SCore.PerformanceMonitor.ResetCollection(name); - monitor.Log($"All performance counters for category {name} are now cleared.", LogLevel.Info); - break; - case "source": - SCore.PerformanceMonitor.ResetSource(name); - monitor.Log($"All performance counters for source {name} are now cleared.", LogLevel.Info); - break; - } - } - else - { - SCore.PerformanceMonitor.Reset(); - monitor.Log("All performance counters are now cleared.", LogLevel.Info); - } - } - - /// Formats the given milliseconds value into a string format. Optionally - /// allows a threshold to return "-" if the value is less than the threshold. - /// The milliseconds to format. Returns "-" if null - /// The threshold. Any value below this is returned as "-". - /// The formatted milliseconds. - private string FormatMilliseconds(double? milliseconds, double? thresholdMilliseconds = null) - { - thresholdMilliseconds ??= 1; - return milliseconds != null && milliseconds >= thresholdMilliseconds - ? ((double)milliseconds).ToString("F2") - : "-"; - } - - /// Shows detailed help for a specific sub command. - /// The output monitor. - /// The subcommand. - private void OutputHelp(IMonitor monitor, SubCommand? subcommand) - { - StringBuilder report = new StringBuilder(); - report.AppendLine(); - - switch (subcommand) - { - case SubCommand.Detail: - report.AppendLine($" {CommandName} detail "); - report.AppendLine(); - report.AppendLine("Displays details for a specific collection."); - report.AppendLine(); - report.AppendLine("Arguments:"); - report.AppendLine(" Optional. The threshold in milliseconds. Any average execution time below that"); - report.AppendLine(" threshold is not reported."); - report.AppendLine(); - report.AppendLine("Examples:"); - report.AppendLine($"{CommandName} detail 5 Show counters exceeding an average of 5ms"); - break; - - case SubCommand.Summary: - report.AppendLine($"Usage: {CommandName} summary "); - report.AppendLine(); - report.AppendLine("Displays the performance counter summary."); - report.AppendLine(); - report.AppendLine("Arguments:"); - report.AppendLine(" Optional. Hides the actual execution time if it's below this threshold"); - report.AppendLine(); - report.AppendLine("Examples:"); - report.AppendLine($"{CommandName} summary Show all events"); - report.AppendLine($"{CommandName} summary 5 Shows events exceeding an average of 5ms"); - break; - - case SubCommand.Trigger: - report.AppendLine($"Usage: {CommandName} trigger "); - report.AppendLine($"Usage: {CommandName} trigger collection "); - report.AppendLine($"Usage: {CommandName} trigger collection "); - report.AppendLine(); - report.AppendLine("Manages alert triggers."); - report.AppendLine(); - report.AppendLine("Arguments:"); - report.AppendLine(" Optional. Specifies if a specific source or a specific collection should be triggered."); - report.AppendLine(" - list Lists current triggers"); - report.AppendLine(" - collection Sets up a trigger for a collection"); - report.AppendLine(" - clear Clears all trigger entries"); - report.AppendLine(" - pause Pauses triggering of alerts"); - report.AppendLine(" - resume Resumes triggering of alerts"); - report.AppendLine(" - dump Dumps all triggers as commands for copy and paste"); - report.AppendLine(" Defaults to 'list' if not specified."); - report.AppendLine(); - report.AppendLine(" Required if the mode 'collection' is specified."); - report.AppendLine(" Specifies the name of the collection to be triggered. Must be an exact match."); - report.AppendLine(); - report.AppendLine(" Optional. Specifies the name of a specific source. Must be an exact match."); - report.AppendLine(); - report.AppendLine(" Required if the mode 'collection' is specified."); - report.AppendLine(" Specifies the threshold in milliseconds (fractions allowed)."); - report.AppendLine(" Specify '0' to remove the threshold."); - report.AppendLine(); - report.AppendLine("Examples:"); - report.AppendLine(); - report.AppendLine($"{CommandName} trigger collection Display.Rendering 10"); - report.AppendLine(" Sets up an alert trigger which writes on the console if the execution time of all performance counters in"); - report.AppendLine(" the 'Display.Rendering' collection exceed 10 milliseconds."); - report.AppendLine(); - report.AppendLine($"{CommandName} trigger collection Display.Rendering 5 Pathoschild.ChestsAnywhere"); - report.AppendLine(" Sets up an alert trigger to write on the console if the execution time of Pathoschild.ChestsAnywhere in"); - report.AppendLine(" the 'Display.Rendering' collection exceed 5 milliseconds."); - report.AppendLine(); - report.AppendLine($"{CommandName} trigger collection Display.Rendering 0"); - report.AppendLine(" Removes the threshold previously defined from the collection. Note that source-specific thresholds are left intact."); - report.AppendLine(); - report.AppendLine($"{CommandName} trigger clear"); - report.AppendLine(" Clears all previously setup alert triggers."); - break; - - case SubCommand.Reset: - report.AppendLine($"Usage: {CommandName} reset "); - report.AppendLine(); - report.AppendLine("Resets performance counters."); - report.AppendLine(); - report.AppendLine("Arguments:"); - report.AppendLine(" Optional. Specifies if a collection or source should be reset."); - report.AppendLine(" If omitted, all performance counters are reset."); - report.AppendLine(); - report.AppendLine(" - source Clears performance counters for a specific source"); - report.AppendLine(" - collection Clears performance counters for a specific collection"); - report.AppendLine(); - report.AppendLine(" Required if a is given. Specifies the name of either the collection"); - report.AppendLine(" or the source. The name must be an exact match."); - report.AppendLine(); - report.AppendLine("Examples:"); - report.AppendLine($"{CommandName} reset Resets all performance counters"); - report.AppendLine($"{CommandName} reset source Pathoschild.ChestsAnywhere Resets all performance for the source named Pathoschild.ChestsAnywhere"); - report.AppendLine($"{CommandName} reset collection Display.Rendering Resets all performance for the collection named Display.Rendering"); - break; - } - - report.AppendLine(); - monitor.Log(report.ToString(), LogLevel.Info); - } - - /// Get the command description. - private static string GetDescription() - { - StringBuilder report = new StringBuilder(); - - report.AppendLine("Displays or configures performance monitoring to diagnose issues. Performance monitoring is disabled by default."); - report.AppendLine(); - report.AppendLine("For example, the counter collection named 'Display.Rendered' contains one performance"); - report.AppendLine("counter when the game executes the 'Display.Rendered' event, and another counter for each mod which handles it."); - report.AppendLine(); - report.AppendLine($"Usage: {CommandName} "); - report.AppendLine(); - report.AppendLine("Commands:"); - report.AppendLine(); - report.AppendLine(" summary Show a summary of collections."); - report.AppendLine(" detail Show a summary for a given collection."); - report.AppendLine(" reset Reset all performance counters."); - report.AppendLine(" trigger Configure alert triggers."); - report.AppendLine(" enable Enable performance counter recording."); - report.AppendLine(" disable Disable performance counter recording."); - report.AppendLine(" help Show verbose help for the available commands."); - report.AppendLine(); - report.AppendLine($"To get help for a specific command, use '{CommandName} help ', for example:"); - report.AppendLine($"{CommandName} help summary"); - report.AppendLine(); - report.AppendLine("Defaults to summary if no command is given."); - report.AppendLine(); - - return report.ToString(); - } - - /// Log a warning if performance monitoring isn't enabled. - /// Writes messages to the console and log file. - /// Returns whether performance monitoring is enabled. - private bool AssertEnabled(IMonitor monitor) - { - if (!SCore.PerformanceMonitor.EnableTracking) - { - monitor.Log($"Performance monitoring is currently disabled; enter '{CommandName} enable' to enable it.", LogLevel.Warn); - return false; - } - - return true; - } - - - /********* - ** Private models - *********/ - /// An alert trigger for a collection. - private class CollectionTrigger - { - /********* - ** Accessors - *********/ - /// The collection name. - public string CollectionName { get; } - - /// The trigger threshold. - public double Threshold { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The collection name. - /// The trigger threshold. - public CollectionTrigger(string collectionName, double threshold) - { - this.CollectionName = collectionName; - this.Threshold = threshold; - } - } - - /// An alert triggered for a source. - private class SourceTrigger : CollectionTrigger - { - /********* - ** Accessors - *********/ - /// The source name. - public string SourceName { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The collection name. - /// The source name. - /// The trigger threshold. - public SourceTrigger(string collectionName, string sourceName, double threshold) - : base(collectionName, threshold) - { - this.SourceName = sourceName; - } - } - } -} diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/TrainerCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/TrainerCommand.cs index 77a26c6a..98daa906 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/TrainerCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/TrainerCommand.cs @@ -78,8 +78,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands /// The data to display. /// The table header. /// Returns a set of fields for a data value. - /// Whether to right-align the data. - protected string GetTableString(IEnumerable data, string[] header, Func getRow, bool rightAlign = false) + protected string GetTableString(IEnumerable data, string[] header, Func getRow) { // get table data int[] widths = header.Select(p => p.Length).ToArray(); @@ -108,7 +107,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands return string.Join( Environment.NewLine, lines.Select(line => string.Join(" | ", - line.Select((field, i) => rightAlign ? field.PadRight(widths[i], ' ') : field.PadLeft(widths[i], ' ')) + line.Select((field, i) => field.PadLeft(widths[i], ' ')) )) ); } diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index ef996c0f..9816013c 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -97,9 +97,6 @@ namespace StardewModdingAPI /// The URL of the SMAPI home page. internal const string HomePageUrl = "https://smapi.io"; - /// The default performance counter name for unknown event handlers. - internal const string GamePerformanceCounterName = ""; - /// The absolute path to the folder containing SMAPI's internal files. internal static readonly string InternalFilesPath = EarlyConstants.InternalFilesPath; diff --git a/src/SMAPI/Framework/Events/EventManager.cs b/src/SMAPI/Framework/Events/EventManager.cs index 9092669f..665dbfe3 100644 --- a/src/SMAPI/Framework/Events/EventManager.cs +++ b/src/SMAPI/Framework/Events/EventManager.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Reflection; using StardewModdingAPI.Events; -using StardewModdingAPI.Framework.PerformanceMonitoring; namespace StardewModdingAPI.Framework.Events { @@ -178,13 +177,12 @@ namespace StardewModdingAPI.Framework.Events *********/ /// Construct an instance. /// The mod registry with which to identify mods. - /// Tracks performance metrics. - public EventManager(ModRegistry modRegistry, PerformanceMonitor performanceMonitor) + public EventManager(ModRegistry modRegistry) { // create shortcut initializers ManagedEvent ManageEventOf(string typeName, string eventName, bool isPerformanceCritical = false) { - return new ManagedEvent($"{typeName}.{eventName}", modRegistry, performanceMonitor, isPerformanceCritical); + return new ManagedEvent($"{typeName}.{eventName}", modRegistry, isPerformanceCritical); } // init events (new) diff --git a/src/SMAPI/Framework/Events/ManagedEvent.cs b/src/SMAPI/Framework/Events/ManagedEvent.cs index f2dfb2ab..2204966c 100644 --- a/src/SMAPI/Framework/Events/ManagedEvent.cs +++ b/src/SMAPI/Framework/Events/ManagedEvent.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; using StardewModdingAPI.Events; -using StardewModdingAPI.Framework.PerformanceMonitoring; namespace StardewModdingAPI.Framework.Events { @@ -17,9 +16,6 @@ namespace StardewModdingAPI.Framework.Events /// The mod registry with which to identify mods. protected readonly ModRegistry ModRegistry; - /// Tracks performance metrics. - private readonly PerformanceMonitor PerformanceMonitor; - /// The underlying event handlers. private readonly List> Handlers = new List>(); @@ -49,13 +45,11 @@ namespace StardewModdingAPI.Framework.Events /// Construct an instance. /// A human-readable name for the event. /// The mod registry with which to identify mods. - /// Tracks performance metrics. /// Whether the event is typically called at least once per second. - public ManagedEvent(string eventName, ModRegistry modRegistry, PerformanceMonitor performanceMonitor, bool isPerformanceCritical = false) + public ManagedEvent(string eventName, ModRegistry modRegistry, bool isPerformanceCritical = false) { this.EventName = eventName; this.ModRegistry = modRegistry; - this.PerformanceMonitor = performanceMonitor; this.IsPerformanceCritical = isPerformanceCritical; } @@ -126,40 +120,26 @@ namespace StardewModdingAPI.Framework.Events } // raise event - this.PerformanceMonitor.Track(this.EventName, () => + foreach (ManagedEventHandler handler in handlers) { - foreach (ManagedEventHandler handler in handlers) - { - if (match != null && !match(handler.SourceMod)) - continue; + if (match != null && !match(handler.SourceMod)) + continue; - try - { - this.PerformanceMonitor.Track(this.EventName, this.GetModNameForPerformanceCounters(handler), () => handler.Handler.Invoke(null, args)); - } - catch (Exception ex) - { - this.LogError(handler, ex); - } + try + { + handler.Handler.Invoke(null, args); + } + catch (Exception ex) + { + this.LogError(handler, ex); } - }); + } } /********* ** Private methods *********/ - /// Get the mod name for a given event handler to display in performance monitoring reports. - /// The event handler. - private string GetModNameForPerformanceCounters(ManagedEventHandler handler) - { - IModMetadata mod = handler.SourceMod; - - return mod.HasManifest() - ? mod.Manifest.UniqueID - : mod.DisplayName; - } - /// Log an exception from an event handler. /// The event handler instance. /// The exception that was raised. diff --git a/src/SMAPI/Framework/PerformanceMonitoring/AlertContext.cs b/src/SMAPI/Framework/PerformanceMonitoring/AlertContext.cs deleted file mode 100644 index af630055..00000000 --- a/src/SMAPI/Framework/PerformanceMonitoring/AlertContext.cs +++ /dev/null @@ -1,34 +0,0 @@ -namespace StardewModdingAPI.Framework.PerformanceMonitoring -{ - /// The context for an alert. - internal readonly struct AlertContext - { - /********* - ** Accessors - *********/ - /// The source which triggered the alert. - public string Source { get; } - - /// The elapsed milliseconds. - public double Elapsed { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The source which triggered the alert. - /// The elapsed milliseconds. - public AlertContext(string source, double elapsed) - { - this.Source = source; - this.Elapsed = elapsed; - } - - /// Get a human-readable text form of this instance. - public override string ToString() - { - return $"{this.Source}: {this.Elapsed:F2}ms"; - } - } -} diff --git a/src/SMAPI/Framework/PerformanceMonitoring/AlertEntry.cs b/src/SMAPI/Framework/PerformanceMonitoring/AlertEntry.cs deleted file mode 100644 index d5a0b343..00000000 --- a/src/SMAPI/Framework/PerformanceMonitoring/AlertEntry.cs +++ /dev/null @@ -1,38 +0,0 @@ -namespace StardewModdingAPI.Framework.PerformanceMonitoring -{ - /// A single alert entry. - internal readonly struct AlertEntry - { - /********* - ** Accessors - *********/ - /// The collection in which the alert occurred. - public PerformanceCounterCollection Collection { get; } - - /// The actual execution time in milliseconds. - public double ExecutionTimeMilliseconds { get; } - - /// The configured alert threshold in milliseconds. - public double ThresholdMilliseconds { get; } - - /// The sources involved in exceeding the threshold. - public AlertContext[] Context { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The collection in which the alert occurred. - /// The actual execution time in milliseconds. - /// The configured alert threshold in milliseconds. - /// The sources involved in exceeding the threshold. - public AlertEntry(PerformanceCounterCollection collection, double executionTimeMilliseconds, double thresholdMilliseconds, AlertContext[] context) - { - this.Collection = collection; - this.ExecutionTimeMilliseconds = executionTimeMilliseconds; - this.ThresholdMilliseconds = thresholdMilliseconds; - this.Context = context; - } - } -} diff --git a/src/SMAPI/Framework/PerformanceMonitoring/PeakEntry.cs b/src/SMAPI/Framework/PerformanceMonitoring/PeakEntry.cs deleted file mode 100644 index 1746e358..00000000 --- a/src/SMAPI/Framework/PerformanceMonitoring/PeakEntry.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System; - -namespace StardewModdingAPI.Framework.PerformanceMonitoring -{ - /// A peak invocation time. - internal readonly struct PeakEntry - { - /********* - ** Accessors - *********/ - /// The actual execution time in milliseconds. - public double ExecutionTimeMilliseconds { get; } - - /// When the entry occurred. - public DateTime EventTime { get; } - - /// The sources involved in exceeding the threshold. - public AlertContext[] Context { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The actual execution time in milliseconds. - /// When the entry occurred. - /// The sources involved in exceeding the threshold. - public PeakEntry(double executionTimeMilliseconds, DateTime eventTime, AlertContext[] context) - { - this.ExecutionTimeMilliseconds = executionTimeMilliseconds; - this.EventTime = eventTime; - this.Context = context; - } - } -} diff --git a/src/SMAPI/Framework/PerformanceMonitoring/PerformanceCounter.cs b/src/SMAPI/Framework/PerformanceMonitoring/PerformanceCounter.cs deleted file mode 100644 index 42825999..00000000 --- a/src/SMAPI/Framework/PerformanceMonitoring/PerformanceCounter.cs +++ /dev/null @@ -1,124 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; - -namespace StardewModdingAPI.Framework.PerformanceMonitoring -{ - /// Tracks metadata about a particular code event. - internal class PerformanceCounter - { - /********* - ** Fields - *********/ - /// The size of the ring buffer. - private readonly int MaxEntries = 16384; - - /// The collection to which this performance counter belongs. - private readonly PerformanceCounterCollection ParentCollection; - - /// The performance counter entries. - private readonly Stack Entries; - - /// The entry with the highest execution time. - private PerformanceCounterEntry? PeakPerformanceCounterEntry; - - - /********* - ** Accessors - *********/ - /// The name of the source. - public string Source { get; } - - /// The alert threshold in milliseconds - public double AlertThresholdMilliseconds { get; set; } - - /// If alerting is enabled or not - public bool EnableAlerts { get; set; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The collection to which this performance counter belongs. - /// The name of the source. - public PerformanceCounter(PerformanceCounterCollection parentCollection, string source) - { - this.ParentCollection = parentCollection; - this.Source = source; - this.Entries = new Stack(this.MaxEntries); - } - - /// Add a performance counter entry to the list, update monitoring, and raise alerts if needed. - /// The entry to add. - public void Add(PerformanceCounterEntry entry) - { - // add entry - if (this.Entries.Count > this.MaxEntries) - this.Entries.Pop(); - this.Entries.Push(entry); - - // update metrics - if (this.PeakPerformanceCounterEntry == null || entry.ElapsedMilliseconds > this.PeakPerformanceCounterEntry.Value.ElapsedMilliseconds) - this.PeakPerformanceCounterEntry = entry; - - // raise alert - if (this.EnableAlerts && entry.ElapsedMilliseconds > this.AlertThresholdMilliseconds) - this.ParentCollection.AddAlert(entry.ElapsedMilliseconds, this.AlertThresholdMilliseconds, new AlertContext(this.Source, entry.ElapsedMilliseconds)); - } - - /// Clear all performance counter entries and monitoring. - public void Reset() - { - this.Entries.Clear(); - this.PeakPerformanceCounterEntry = null; - } - - /// Get the peak entry. - public PerformanceCounterEntry? GetPeak() - { - return this.PeakPerformanceCounterEntry; - } - - /// Get the entry with the highest execution time. - /// The time range to search. - /// The end time for the , or null for the current time. - public PerformanceCounterEntry? GetPeak(TimeSpan range, DateTime? endTime = null) - { - endTime ??= DateTime.UtcNow; - DateTime startTime = endTime.Value.Subtract(range); - - return this.Entries - .Where(ent