From 372eb722334b9d0cc463be6fc418081a65d04717 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 5 Jan 2020 23:08:17 -0500 Subject: streamline front page design --- src/SMAPI.Web/Views/Index/Index.cshtml | 157 ++++++++++++++-------------- src/SMAPI.Web/Views/Shared/_Layout.cshtml | 7 +- src/SMAPI.Web/wwwroot/Content/css/index.css | 35 +++++-- 3 files changed, 113 insertions(+), 86 deletions(-) (limited to 'src') diff --git a/src/SMAPI.Web/Views/Index/Index.cshtml b/src/SMAPI.Web/Views/Index/Index.cshtml index 5d91dc84..778da2d1 100644 --- a/src/SMAPI.Web/Views/Index/Index.cshtml +++ b/src/SMAPI.Web/Views/Index/Index.cshtml @@ -1,4 +1,3 @@ -@using Markdig @using Microsoft.Extensions.Options @using StardewModdingAPI.Web.Framework @using StardewModdingAPI.Web.Framework.ConfigModels @@ -6,18 +5,22 @@ @model StardewModdingAPI.Web.ViewModels.IndexModel @{ ViewData["Title"] = "SMAPI"; + ViewData["ViewTitle"] = string.Empty; } @section Head { - + - + } -

- The mod loader for Stardew Valley. It works fine with GOG and Steam achievements, it's - compatible with Linux/Mac/Windows, you can uninstall it anytime, and there's a friendly - community if you need help. It's a cool pufferchick. -

+

+ SMAPI + +

+
+

The mod loader for Stardew Valley.

+

Compatible with GOG/Steam achievements and Linux/Mac/Windows, uninstall anytime, and there's a friendly community if you need help.

+
@@ -45,80 +48,82 @@

}
Player guide
- -
-

Get help

- - -@if (Model.BetaVersion == null) -{ -

What's new in SMAPI @Model.StableVersion.Version?

-
- @Html.Raw(Markdig.Markdown.ToHtml(Model.StableVersion.Description)) -
-

See the release notes and mod compatibility list for more info.

-} -else -{ -

What's new in...

-

SMAPI @Model.StableVersion.Version?

-
- @Html.Raw(Markdig.Markdown.ToHtml(Model.StableVersion.Description)) -
-

See the release notes and mod compatibility list for more info.

+
+

Get help

+ +
-

SMAPI @Model.BetaVersion.Version?

-
- @Html.Raw(Markdig.Markdown.ToHtml(Model.BetaVersion.Description)) -
-

See the release notes and mod compatibility list for more info.

-} +
+ @if (Model.BetaVersion == null) + { +

What's new

+
+ @Html.Raw(Markdig.Markdown.ToHtml(Model.StableVersion.Description)) +
+

See the release notes and mod compatibility list for more info.

+ } + else + { +

What's new in...

+

SMAPI @Model.StableVersion.Version?

+
+ @Html.Raw(Markdig.Markdown.ToHtml(Model.StableVersion.Description)) +
+

See the release notes and mod compatibility list for more info.

- -

- SMAPI is an open-source project by Pathoschild. It will always be free, but donations - are much appreciated to help pay for development, server hosting, domain fees, coffee, etc. -

+

SMAPI @Model.BetaVersion.Version?

+
+ @Html.Raw(Markdig.Markdown.ToHtml(Model.BetaVersion.Description)) +
+

See the release notes and mod compatibility list for more info.

+ } +
- +
+ +

+ SMAPI is an open-source project by Pathoschild. It will always be free, but donations + are much appreciated to help pay for development, server hosting, domain fees, coffee, etc. +

-@if (!string.IsNullOrWhiteSpace(Model.SupporterList)) -{ - @Html.Raw(Markdig.Markdown.ToHtml( - $"Special thanks to {Model.SupporterList}, and a few anonymous users for their ongoing support on Patreon; you're awesome!" - )) -} + -

For mod creators

- +
+ +

For mod creators

+ diff --git a/src/SMAPI.Web/Views/Shared/_Layout.cshtml b/src/SMAPI.Web/Views/Shared/_Layout.cshtml index 17f1f673..2d06ceb1 100644 --- a/src/SMAPI.Web/Views/Shared/_Layout.cshtml +++ b/src/SMAPI.Web/Views/Shared/_Layout.cshtml @@ -29,12 +29,15 @@
-

@(ViewData["ViewTitle"] ?? ViewData["Title"])

+ @if (ViewData["ViewTitle"] != string.Empty) + { +

@(ViewData["ViewTitle"] ?? ViewData["Title"])

+ } @RenderBody()
diff --git a/src/SMAPI.Web/wwwroot/Content/css/index.css b/src/SMAPI.Web/wwwroot/Content/css/index.css index 93a85bed..1cf8d261 100644 --- a/src/SMAPI.Web/wwwroot/Content/css/index.css +++ b/src/SMAPI.Web/wwwroot/Content/css/index.css @@ -21,12 +21,10 @@ h1 { #call-to-action a.main-cta, #call-to-action a.secondary-cta { box-shadow: #caefab 0 1px 0 0 inset; - background: linear-gradient(#77d42a 5%, #5cb811 100%) #77d42a; border-radius: 6px; border: 1px solid #268a16; display: inline-block; cursor: pointer; - color: #306108; font-weight: bold; margin-bottom: 1em; padding: 6px 24px; @@ -34,10 +32,16 @@ h1 { text-shadow: #aade7c 0 1px 0; } +#call-to-action a.main-cta { + background: linear-gradient(#77d42a 5%, #5cb811 75%) #77d42a; + font-size: 1.5em; + color: #306108; +} + #call-to-action a.secondary-cta { background: #768d87; border: 1px solid #566963; - color: #ffffff; + color: #eee; text-shadow: #2b665e 0 1px 0; } @@ -101,9 +105,24 @@ h1 { /********* ** Subsections *********/ -.github-description { - border-left: 0.25em solid #dfe2e5; - padding-left: 1em; +.area { + background: rgba(0, 170, 0, 0.2); + padding: 0 1em 1em 1em; + margin-bottom: 1em; +} + +.area > ul, +.area > div, +.area > p { + margin-left: 3em; +} + +.area > ul { + padding-left: 0; +} + +.area > h2 { + border: 0; } #donate-links li { @@ -114,12 +133,12 @@ h1 { #donate-links .donate-button { display: inline-block; min-width: 10em; - background: #2A413B; + background: #2a413b; padding: 6px 12px; font-family: Quicksand, Helvetica, Century Gothic, sans-serif; text-decoration: none; font-weight: 700; - color: #FFF; + color: #fff; border-radius: 8px; } -- cgit From b8a566a060eb5caa8cc37edba3ca670192f7a35b Mon Sep 17 00:00:00 2001 From: kchapelier Date: Mon, 6 Jan 2020 09:27:24 +0100 Subject: Add french translation --- docs/README.md | 2 +- src/SMAPI/SMAPI.csproj | 3 +++ src/SMAPI/i18n/fr.json | 3 +++ 3 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 src/SMAPI/i18n/fr.json (limited to 'src') diff --git a/docs/README.md b/docs/README.md index 3a570f48..50478b52 100644 --- a/docs/README.md +++ b/docs/README.md @@ -64,7 +64,7 @@ locale | status ---------- | :---------------- default | ✓ [fully translated](../src/SMAPI/i18n/default.json) Chinese | ✓ [fully translated](../src/SMAPI/i18n/zh.json) -French | ❑ not translated +French | ✓ [fully translated](../src/SMAPI/i18n/fr.json) German | ✓ [fully translated](../src/SMAPI/i18n/de.json) Hungarian | ❑ not translated Italian | ❑ not translated diff --git a/src/SMAPI/SMAPI.csproj b/src/SMAPI/SMAPI.csproj index 936c420d..3bb73295 100644 --- a/src/SMAPI/SMAPI.csproj +++ b/src/SMAPI/SMAPI.csproj @@ -105,6 +105,9 @@ PreserveNewest + + PreserveNewest + PreserveNewest diff --git a/src/SMAPI/i18n/fr.json b/src/SMAPI/i18n/fr.json new file mode 100644 index 00000000..6d051025 --- /dev/null +++ b/src/SMAPI/i18n/fr.json @@ -0,0 +1,3 @@ +{ + "warn.invalid-content-removed": "Le contenu non valide a été supprimé afin d'éviter un plantage (voir la console de SMAPI pour plus d'informations)." +} -- cgit From 18c69c5587f1196afc5c380cb078157e71b1a385 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 7 Jan 2020 21:26:58 -0500 Subject: intercept schedule errors --- docs/release-notes.md | 5 ++ src/SMAPI/Framework/SCore.cs | 3 +- src/SMAPI/Patches/ScheduleErrorPatch.cs | 86 +++++++++++++++++++++++++++++++++ 3 files changed, 93 insertions(+), 1 deletion(-) create mode 100644 src/SMAPI/Patches/ScheduleErrorPatch.cs (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index ed6f9013..cf8fee7a 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -1,6 +1,11 @@ ← [README](README.md) # Release notes +## Upcoming release + +* For players: + * SMAPI now prevents mods from crashing the game with invalid schedule data. + ## 3.1 Released 05 January 2019 for Stardew Valley 1.4 or later. diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index dfd77e16..c4841ece 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -253,7 +253,8 @@ namespace StardewModdingAPI.Framework new DialogueErrorPatch(this.MonitorForGame, this.Reflection), new ObjectErrorPatch(), new LoadContextPatch(this.Reflection, this.GameInstance.OnLoadStageChanged), - new LoadErrorPatch(this.Monitor, this.GameInstance.OnSaveContentRemoved) + new LoadErrorPatch(this.Monitor, this.GameInstance.OnSaveContentRemoved), + new ScheduleErrorPatch(this.MonitorForGame) ); // add exit handler diff --git a/src/SMAPI/Patches/ScheduleErrorPatch.cs b/src/SMAPI/Patches/ScheduleErrorPatch.cs new file mode 100644 index 00000000..a23aa645 --- /dev/null +++ b/src/SMAPI/Patches/ScheduleErrorPatch.cs @@ -0,0 +1,86 @@ +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Reflection; +using Harmony; +using StardewModdingAPI.Framework.Patching; +using StardewValley; + +namespace StardewModdingAPI.Patches +{ + /// A Harmony patch for which intercepts crashes due to invalid schedule data. + /// Patch methods must be static for Harmony to work correctly. See the Harmony documentation before renaming patch arguments. + [SuppressMessage("ReSharper", "InconsistentNaming", Justification = "Argument names are defined by Harmony and methods are named for clarity.")] + [SuppressMessage("ReSharper", "IdentifierTypo", Justification = "Argument names are defined by Harmony and methods are named for clarity.")] + internal class ScheduleErrorPatch : IHarmonyPatch + { + /********* + ** Fields + *********/ + /// Writes messages to the console and log file on behalf of the game. + private static IMonitor MonitorForGame; + + /// Whether the target is currently being intercepted. + private static bool IsIntercepting; + + + /********* + ** Accessors + *********/ + /// A unique name for this patch. + public string Name => nameof(ScheduleErrorPatch); + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// Writes messages to the console and log file on behalf of the game. + public ScheduleErrorPatch(IMonitor monitorForGame) + { + ScheduleErrorPatch.MonitorForGame = monitorForGame; + } + + /// Apply the Harmony patch. + /// The Harmony instance. + public void Apply(HarmonyInstance harmony) + { + harmony.Patch( + original: AccessTools.Method(typeof(NPC), "parseMasterSchedule"), + prefix: new HarmonyMethod(this.GetType(), nameof(ScheduleErrorPatch.Before_NPC_parseMasterSchedule)) + ); + } + + + /********* + ** Private methods + *********/ + /// The method to call instead of . + /// The raw schedule data to parse. + /// The instance being patched. + /// The patched method's return value. + /// The method being wrapped. + /// Returns whether to execute the original method. + private static bool Before_NPC_parseMasterSchedule(string rawData, NPC __instance, ref Dictionary __result, MethodInfo __originalMethod) + { + if (ScheduleErrorPatch.IsIntercepting) + return true; + + try + { + ScheduleErrorPatch.IsIntercepting = true; + __result = (Dictionary)__originalMethod.Invoke(__instance, new object[] { rawData }); + return false; + } + catch (TargetInvocationException ex) + { + ScheduleErrorPatch.MonitorForGame.Log($"Failed parsing schedule for NPC {__instance.Name}:\n{rawData}\n{ex.InnerException ?? ex}", LogLevel.Error); + __result = new Dictionary(); + return false; + } + finally + { + ScheduleErrorPatch.IsIntercepting = false; + } + } + } +} -- cgit From ceff27c9a82bb16358aa0f390ce3f346c06c47bc Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Tue, 7 Jan 2020 21:29:49 -0500 Subject: update min game version 1.4.1 is needed due to the new gamepad option, which SMAPI 3.1 added support for. --- docs/release-notes.md | 1 + src/SMAPI/Constants.cs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index cf8fee7a..5aa279b5 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -5,6 +5,7 @@ * For players: * SMAPI now prevents mods from crashing the game with invalid schedule data. + * Updated minimum game version (1.4 → 1.4.1). ## 3.1 Released 05 January 2019 for Stardew Valley 1.4 or later. diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index 97204d86..da2ee375 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -23,7 +23,7 @@ namespace StardewModdingAPI public static ISemanticVersion ApiVersion { get; } = new Toolkit.SemanticVersion("3.1.0"); /// The minimum supported version of Stardew Valley. - public static ISemanticVersion MinimumGameVersion { get; } = new GameVersion("1.4.0"); + public static ISemanticVersion MinimumGameVersion { get; } = new GameVersion("1.4.1"); /// The maximum supported version of Stardew Valley. public static ISemanticVersion MaximumGameVersion { get; } = null; -- cgit From a751252c4ee3b48977d5d24c36a4e4e5466f93db Mon Sep 17 00:00:00 2001 From: Drachenkaetzchen Date: Fri, 10 Jan 2020 01:27:56 +0100 Subject: Initial commit of the performance counters --- build/common.targets | 1 + build/prepare-install-package.targets | 7 +- .../Commands/Other/PerformanceCounterCommand.cs | 14 ++ src/SMAPI/Constants.cs | 4 +- src/SMAPI/Framework/Events/ManagedEvent.cs | 71 +++++- src/SMAPI/Framework/SCore.cs | 247 ++++++++++++++++++++- .../Utilities/EventPerformanceCounterCategory.cs | 16 ++ .../Utilities/IPerformanceCounterEvent.cs | 16 ++ .../Framework/Utilities/PerformanceCounter.cs | 102 +++++++++ .../Framework/Utilities/PerformanceCounterEntry.cs | 10 + src/SMAPI/SMAPI.csproj | 1 + 11 files changed, 482 insertions(+), 7 deletions(-) create mode 100644 src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/PerformanceCounterCommand.cs create mode 100644 src/SMAPI/Framework/Utilities/EventPerformanceCounterCategory.cs create mode 100644 src/SMAPI/Framework/Utilities/IPerformanceCounterEvent.cs create mode 100644 src/SMAPI/Framework/Utilities/PerformanceCounter.cs create mode 100644 src/SMAPI/Framework/Utilities/PerformanceCounterEntry.cs (limited to 'src') diff --git a/build/common.targets b/build/common.targets index df2d4861..78b435d0 100644 --- a/build/common.targets +++ b/build/common.targets @@ -32,6 +32,7 @@ + diff --git a/build/prepare-install-package.targets b/build/prepare-install-package.targets index 4297756d..96716ecb 100644 --- a/build/prepare-install-package.targets +++ b/build/prepare-install-package.targets @@ -1,9 +1,9 @@ @@ -24,7 +24,7 @@ - + @@ -41,6 +41,7 @@ + diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/PerformanceCounterCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/PerformanceCounterCommand.cs new file mode 100644 index 00000000..b7e56359 --- /dev/null +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/PerformanceCounterCommand.cs @@ -0,0 +1,14 @@ +namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other +{ + internal class PerformanceCounterCommand: TrainerCommand + { + public PerformanceCounterCommand(string name, string description) : base("performance_counters", "Displays performance counters") + { + } + + public override void Handle(IMonitor monitor, string command, ArgumentParser args) + { + + } + } +} diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index da2ee375..0923494c 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -55,6 +55,9 @@ namespace StardewModdingAPI /// The URL of the SMAPI home page. internal const string HomePageUrl = "https://smapi.io"; + /// The URL of the SMAPI home page. + internal const string GamePerformanceCounterName = "-internal-"; + /// The absolute path to the folder containing SMAPI's internal files. internal static readonly string InternalFilesPath = Program.DllSearchPath; @@ -100,7 +103,6 @@ namespace StardewModdingAPI /// The language code for non-translated mod assets. internal static LocalizedContentManager.LanguageCode DefaultLanguage { get; } = LocalizedContentManager.LanguageCode.en; - /********* ** Internal methods *********/ diff --git a/src/SMAPI/Framework/Events/ManagedEvent.cs b/src/SMAPI/Framework/Events/ManagedEvent.cs index 2afe7a03..9a5cb174 100644 --- a/src/SMAPI/Framework/Events/ManagedEvent.cs +++ b/src/SMAPI/Framework/Events/ManagedEvent.cs @@ -1,12 +1,15 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using System.Linq; +using StardewModdingAPI.Framework.Utilities; +using PerformanceCounter = StardewModdingAPI.Framework.Utilities.PerformanceCounter; namespace StardewModdingAPI.Framework.Events { /// An event wrapper which intercepts and logs errors in handler code. /// The event arguments type. - internal class ManagedEvent + internal class ManagedEvent: IPerformanceCounterEvent { /********* ** Fields @@ -29,6 +32,38 @@ namespace StardewModdingAPI.Framework.Events /// The cached invocation list. private EventHandler[] CachedInvocationList; + public IDictionary PerformanceCounters { get; } = new Dictionary(); + + private readonly Stopwatch Stopwatch = new Stopwatch(); + + private long EventCallCount = 0; + + private readonly DateTime StartDateTime = DateTime.Now; + + public string GetEventName() + { + return this.EventName; + } + + public double GetGameAverageExecutionTime() + { + if (this.PerformanceCounters.TryGetValue(Constants.GamePerformanceCounterName, out PerformanceCounter gameExecTime)) + { + return gameExecTime.GetAverage(); + } + + return 0; + } + + public double GetModsAverageExecutionTime() + { + return this.PerformanceCounters.Where(p => p.Key != Constants.GamePerformanceCounterName).Sum(p => p.Value.GetAverage()); + } + + public double GetAverageExecutionTime() + { + return this.PerformanceCounters.Sum(p => p.Value.GetAverage()); + } /********* ** Public methods @@ -64,6 +99,8 @@ namespace StardewModdingAPI.Framework.Events { this.Event += handler; this.AddTracking(mod, handler, this.Event?.GetInvocationList().Cast>()); + + } /// Remove an event handler. @@ -74,6 +111,18 @@ namespace StardewModdingAPI.Framework.Events this.RemoveTracking(handler, this.Event?.GetInvocationList().Cast>()); } + public long GetAverageCallsPerSecond() + { + long runtimeInSeconds = (long)DateTime.Now.Subtract(this.StartDateTime).TotalSeconds; + + if (runtimeInSeconds == 0) + { + return 0; + } + + return this.EventCallCount / runtimeInSeconds; + } + /// Raise the event and notify all handlers. /// The event arguments to pass. public void Raise(TEventArgs args) @@ -81,11 +130,31 @@ namespace StardewModdingAPI.Framework.Events if (this.Event == null) return; + this.EventCallCount++; + foreach (EventHandler handler in this.CachedInvocationList) { try { + var performanceCounterEntry = new PerformanceCounterEntry() + { + EventTime = DateTime.Now + }; + + this.Stopwatch.Reset(); + this.Stopwatch.Start(); handler.Invoke(null, args); + this.Stopwatch.Stop(); + performanceCounterEntry.Elapsed = this.Stopwatch.Elapsed; + + string modName = this.GetSourceMod(handler)?.DisplayName ?? Constants.GamePerformanceCounterName; + + if (!this.PerformanceCounters.ContainsKey(modName)) + { + this.PerformanceCounters.Add(modName, new PerformanceCounter($"{modName}.{this.EventName}")); + } + this.PerformanceCounters[modName].Add(performanceCounterEntry); + } catch (Exception ex) { diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index c4841ece..6946a817 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -25,6 +25,7 @@ using StardewModdingAPI.Framework.ModLoading; using StardewModdingAPI.Framework.Patching; using StardewModdingAPI.Framework.Reflection; using StardewModdingAPI.Framework.Serialization; +using StardewModdingAPI.Framework.Utilities; using StardewModdingAPI.Patches; using StardewModdingAPI.Toolkit; using StardewModdingAPI.Toolkit.Framework.Clients.WebApi; @@ -33,6 +34,7 @@ using StardewModdingAPI.Toolkit.Serialization; using StardewModdingAPI.Toolkit.Utilities; using StardewValley; using Object = StardewValley.Object; +using PerformanceCounter = StardewModdingAPI.Framework.Utilities.PerformanceCounter; using ThreadState = System.Threading.ThreadState; namespace StardewModdingAPI.Framework @@ -77,6 +79,8 @@ namespace StardewModdingAPI.Framework /// This is initialized after the game starts. private readonly ModRegistry ModRegistry = new ModRegistry(); + private HashSet PerformanceCounterEvents = new HashSet(); + /// Manages SMAPI events for mods. private readonly EventManager EventManager; @@ -109,7 +113,7 @@ namespace StardewModdingAPI.Framework "Oops! Steam achievements won't work because Steam isn't loaded. You can launch the game through Steam to fix that.", #endif logLevel: LogLevel.Error - ), + ), // save file not found error new ReplaceLogPattern( @@ -162,6 +166,8 @@ namespace StardewModdingAPI.Framework }; this.MonitorForGame = this.GetSecondaryMonitor("game"); this.EventManager = new EventManager(this.Monitor, this.ModRegistry); + this.InitializePerformanceCounterEvents(); + SCore.DeprecationManager = new DeprecationManager(this.Monitor, this.ModRegistry); // redirect direct console output @@ -200,6 +206,69 @@ namespace StardewModdingAPI.Framework #endif } + private void InitializePerformanceCounterEvents() + { + this.PerformanceCounterEvents = new HashSet() + { + new EventPerformanceCounterCategory(this.EventManager.MenuChanged, false), + + // Rendering Events + new EventPerformanceCounterCategory(this.EventManager.Rendering, true), + new EventPerformanceCounterCategory(this.EventManager.Rendered, true), + new EventPerformanceCounterCategory(this.EventManager.RenderingWorld, true), + new EventPerformanceCounterCategory(this.EventManager.RenderedWorld, true), + new EventPerformanceCounterCategory(this.EventManager.RenderingActiveMenu, true), + new EventPerformanceCounterCategory(this.EventManager.RenderedActiveMenu, true), + new EventPerformanceCounterCategory(this.EventManager.RenderingHud, true), + new EventPerformanceCounterCategory(this.EventManager.RenderedHud, true), + + new EventPerformanceCounterCategory(this.EventManager.WindowResized, false), + new EventPerformanceCounterCategory(this.EventManager.GameLaunched, false), + new EventPerformanceCounterCategory(this.EventManager.UpdateTicking, true), + new EventPerformanceCounterCategory(this.EventManager.UpdateTicked, true), + new EventPerformanceCounterCategory(this.EventManager.OneSecondUpdateTicking, true), + new EventPerformanceCounterCategory(this.EventManager.OneSecondUpdateTicked, true), + + new EventPerformanceCounterCategory(this.EventManager.SaveCreating, false), + new EventPerformanceCounterCategory(this.EventManager.SaveCreated, false), + new EventPerformanceCounterCategory(this.EventManager.Saving, false), + new EventPerformanceCounterCategory(this.EventManager.Saved, false), + + new EventPerformanceCounterCategory(this.EventManager.DayStarted, false), + new EventPerformanceCounterCategory(this.EventManager.DayEnding, false), + + new EventPerformanceCounterCategory(this.EventManager.TimeChanged, true), + + new EventPerformanceCounterCategory(this.EventManager.ReturnedToTitle, false), + + new EventPerformanceCounterCategory(this.EventManager.ButtonPressed, true), + new EventPerformanceCounterCategory(this.EventManager.ButtonReleased, true), + new EventPerformanceCounterCategory(this.EventManager.CursorMoved, true), + new EventPerformanceCounterCategory(this.EventManager.MouseWheelScrolled, true), + + new EventPerformanceCounterCategory(this.EventManager.PeerContextReceived, true), + new EventPerformanceCounterCategory(this.EventManager.ModMessageReceived, true), + new EventPerformanceCounterCategory(this.EventManager.PeerDisconnected, true), + new EventPerformanceCounterCategory(this.EventManager.InventoryChanged, true), + new EventPerformanceCounterCategory(this.EventManager.LevelChanged, true), + new EventPerformanceCounterCategory(this.EventManager.Warped, true), + + new EventPerformanceCounterCategory(this.EventManager.LocationListChanged, true), + new EventPerformanceCounterCategory(this.EventManager.BuildingListChanged, true), + new EventPerformanceCounterCategory(this.EventManager.LocationListChanged, true), + new EventPerformanceCounterCategory(this.EventManager.DebrisListChanged, true), + new EventPerformanceCounterCategory(this.EventManager.LargeTerrainFeatureListChanged, true), + new EventPerformanceCounterCategory(this.EventManager.NpcListChanged, true), + new EventPerformanceCounterCategory(this.EventManager.ObjectListChanged, true), + new EventPerformanceCounterCategory(this.EventManager.ChestInventoryChanged, true), + new EventPerformanceCounterCategory(this.EventManager.TerrainFeatureListChanged, true), + new EventPerformanceCounterCategory(this.EventManager.LoadStageChanged, false), + new EventPerformanceCounterCategory(this.EventManager.UnvalidatedUpdateTicking, true), + new EventPerformanceCounterCategory(this.EventManager.UnvalidatedUpdateTicked, true), + + }; + } + /// Launch SMAPI. [HandleProcessCorruptedStateExceptions, SecurityCritical] // let try..catch handle corrupted state exceptions public void RunInteractively() @@ -482,6 +551,19 @@ namespace StardewModdingAPI.Framework this.Monitor.Log("Type 'help' for help, or 'help ' for a command's usage", LogLevel.Info); this.GameInstance.CommandManager.Add(null, "help", "Lists command documentation.\n\nUsage: help\nLists all available commands.\n\nUsage: help \n- cmd: The name of a command whose documentation to display.", this.HandleCommand); this.GameInstance.CommandManager.Add(null, "reload_i18n", "Reloads translation files for all mods.\n\nUsage: reload_i18n", this.HandleCommand); + this.GameInstance.CommandManager.Add(null, "performance_counters", + "Displays performance counters.\n\n"+ + "Usage: performance_counters\n" + + "Shows the most important event invocation times\n\n"+ + "Usage: performance_counters summary|sum [all|important|name]\n"+ + "- summary or sum: Forces summary mode\n"+ + "- all, important or name: Displays all event performance counters, only important ones, or a specific event by name (defaults to important)\n\n"+ + "Usage: performance_counters [name] [threshold]\n"+ + "Shows detailed performance counters for a specific event\n"+ + "- name: The (partial) name of the event\n"+ + "- threshold: The minimum avg execution time (ms) of the event\n"+ + "", this.HandleCommand); + this.GameInstance.CommandManager.Add(null, "pc", "Alias for performance_counters", this.HandleCommand); // start handling command line input Thread inputThread = new Thread(() => @@ -1297,12 +1379,173 @@ namespace StardewModdingAPI.Framework this.ReloadTranslations(this.ModRegistry.GetAll(contentPacks: false)); this.Monitor.Log("Reloaded translation files for all mods. This only affects new translations the mods fetch; if they cached some text, it may not be updated.", LogLevel.Info); break; - + case "performance_counters": + case "pc": + this.DisplayPerformanceCounters(arguments.ToList()); + break; default: throw new NotSupportedException($"Unrecognized core SMAPI command '{name}'."); } } + /// Get an ASCII table to show tabular data in the console. + /// The data type. + /// The data to display. + /// The table header. + /// Returns a set of fields for a data value. + protected string GetTableString(IEnumerable data, string[] header, Func getRow) + { + // get table data + int[] widths = header.Select(p => p.Length).ToArray(); + string[][] rows = data + .Select(item => + { + string[] fields = getRow(item); + if (fields.Length != widths.Length) + throw new InvalidOperationException($"Expected {widths.Length} columns, but found {fields.Length}: {string.Join(", ", fields)}"); + + for (int i = 0; i < fields.Length; i++) + widths[i] = Math.Max(widths[i], fields[i].Length); + + return fields; + }) + .ToArray(); + + // render fields + List lines = new List(rows.Length + 2) + { + header, + header.Select((value, i) => "".PadRight(widths[i], '-')).ToArray() + }; + lines.AddRange(rows); + + return string.Join( + Environment.NewLine, + lines.Select(line => string.Join(" | ", line.Select((field, i) => field.PadLeft(widths[i], ' ')).ToArray()) + ) + ); + } + + private void DisplayPerformanceCounters(IList arguments) + { + bool showSummary = true; + bool showSummaryOnlyImportant = true; + string filterByName = null; + + if (arguments.Any()) + { + switch (arguments[0]) + { + case "summary": + case "sum": + showSummary = true; + + if (arguments.Count > 1) + { + switch (arguments[1].ToLower()) + { + case "all": + showSummaryOnlyImportant = false; + break; + case "important": + showSummaryOnlyImportant = true; + break; + default: + filterByName = arguments[1]; + break; + } + } + break; + default: + showSummary = false; + filterByName = arguments[0]; + break; + + } + } + var lastMinute = TimeSpan.FromSeconds(60); + + if (showSummary) + { + this.DisplayPerformanceCounterSummary(showSummaryOnlyImportant, filterByName); + } + else + { + var data = this.PerformanceCounterEvents.Where(p => p.Event.GetEventName().ToLowerInvariant().Contains(filterByName.ToLowerInvariant())); + + foreach (var i in data) + { + this.DisplayPerformanceCounter(i, lastMinute); + } + } + + double avgTime = PerformanceCounter.Stopwatch.ElapsedMilliseconds / (double)PerformanceCounter.EventsLogged; + this.Monitor.Log($"Logged {PerformanceCounter.EventsLogged} events in {PerformanceCounter.Stopwatch.ElapsedMilliseconds}ms (avg {avgTime:F4}ms / event)"); + + } + + private void DisplayPerformanceCounterSummary(bool showOnlyImportant, string eventNameFilter = null) + { + StringBuilder sb = new StringBuilder($"Performance Counter Summary:\n\n"); + + IEnumerable data; + + if (eventNameFilter != null) + { + data = this.PerformanceCounterEvents.Where(p => p.Event.GetEventName().ToLowerInvariant().Contains(eventNameFilter.ToLowerInvariant())); + } + else + { + if (showOnlyImportant) + { + data = this.PerformanceCounterEvents.Where(p => p.IsImportant); + } + else + { + data = this.PerformanceCounterEvents; + } + } + + + sb.AppendLine(this.GetTableString( + data: data, + header: new[] {"Event", "Avg Calls/s", "Avg Execution Time (Game)", "Avg Execution Time (Mods)", "Avg Execution Time (Game+Mods)"}, + getRow: item => new[] + { + item.Event.GetEventName(), + item.Event.GetAverageCallsPerSecond().ToString(), + item.Event.GetGameAverageExecutionTime().ToString("F2") + " ms", + item.Event.GetModsAverageExecutionTime().ToString("F2") + " ms", + item.Event.GetAverageExecutionTime().ToString("F2") + " ms" + } + )); + + this.Monitor.Log(sb.ToString(), LogLevel.Info); + } + + private void DisplayPerformanceCounter (EventPerformanceCounterCategory obj, TimeSpan averageInterval) + { + StringBuilder sb = new StringBuilder($"Performance Counter for {obj.Event.GetEventName()}:\n\n"); + + sb.AppendLine(this.GetTableString( + data: obj.Event.PerformanceCounters, + header: new[] {"Mod", $"Avg Execution Time over {(int)averageInterval.TotalSeconds}s", "Last Execution Time", "Peak Execution Time"}, + getRow: item => new[] + { + item.Key, + item.Value.GetAverage(averageInterval).ToString("F2") + " ms" ?? "-", + item.Value.GetLastEntry()?.Elapsed.TotalMilliseconds.ToString("F2") + " ms" ?? "-", + item.Value.GetPeak()?.Elapsed.TotalMilliseconds.ToString("F2") + " ms" ?? "-", + } + )); + + sb.AppendLine($"Average execution time (Game+Mods): {obj.Event.GetAverageExecutionTime():F2} ms"); + sb.AppendLine($"Average execution time (Game only) : {obj.Event.GetGameAverageExecutionTime():F2} ms"); + sb.AppendLine($"Average execution time (Mods only) : {obj.Event.GetModsAverageExecutionTime():F2} ms"); + + this.Monitor.Log(sb.ToString(), LogLevel.Info); + } + /// Redirect messages logged directly to the console to the given monitor. /// The monitor with which to log messages as the game. /// The message to log. diff --git a/src/SMAPI/Framework/Utilities/EventPerformanceCounterCategory.cs b/src/SMAPI/Framework/Utilities/EventPerformanceCounterCategory.cs new file mode 100644 index 00000000..14f74317 --- /dev/null +++ b/src/SMAPI/Framework/Utilities/EventPerformanceCounterCategory.cs @@ -0,0 +1,16 @@ +namespace StardewModdingAPI.Framework.Utilities +{ + public class EventPerformanceCounterCategory + { + public IPerformanceCounterEvent Event { get; } + public double MonitorThreshold { get; } + public bool IsImportant { get; } + public bool Monitor { get; } + + public EventPerformanceCounterCategory(IPerformanceCounterEvent @event, bool isImportant) + { + this.Event = @event; + this.IsImportant = isImportant; + } + } +} diff --git a/src/SMAPI/Framework/Utilities/IPerformanceCounterEvent.cs b/src/SMAPI/Framework/Utilities/IPerformanceCounterEvent.cs new file mode 100644 index 00000000..55302f90 --- /dev/null +++ b/src/SMAPI/Framework/Utilities/IPerformanceCounterEvent.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; + +namespace StardewModdingAPI.Framework.Utilities +{ + public interface IPerformanceCounterEvent + { + string GetEventName(); + long GetAverageCallsPerSecond(); + IDictionary PerformanceCounters { get; } + + double GetGameAverageExecutionTime(); + double GetModsAverageExecutionTime(); + double GetAverageExecutionTime(); + } +} diff --git a/src/SMAPI/Framework/Utilities/PerformanceCounter.cs b/src/SMAPI/Framework/Utilities/PerformanceCounter.cs new file mode 100644 index 00000000..c9ffcf5b --- /dev/null +++ b/src/SMAPI/Framework/Utilities/PerformanceCounter.cs @@ -0,0 +1,102 @@ +using System; +using System.Diagnostics; +using System.Linq; +using Cyotek.Collections.Generic; + +namespace StardewModdingAPI.Framework.Utilities +{ + public class PerformanceCounter + { + private const int MaxCount = 16384; + + public string Name { get; } + public static Stopwatch Stopwatch = new Stopwatch(); + public static long EventsLogged; + + + private readonly CircularBuffer _counter; + + private PerformanceCounterEntry? PeakPerformanceCounterEntry; + + public PerformanceCounter(string name) + { + this.Name = name; + this._counter = new CircularBuffer(PerformanceCounter.MaxCount); + } + + public int GetAverageCallsPerSecond() + { + var x = this._counter.GroupBy( + p => + (int) p.EventTime.Subtract( + new DateTime(1970, 1, 1) + ).TotalSeconds); + + return x.Last().Count(); + } + + public void Add(PerformanceCounterEntry entry) + { + PerformanceCounter.Stopwatch.Start(); + this._counter.Put(entry); + + if (this.PeakPerformanceCounterEntry == null) + { + this.PeakPerformanceCounterEntry = entry; + } + else + { + if (entry.Elapsed.TotalMilliseconds > this.PeakPerformanceCounterEntry.Value.Elapsed.TotalMilliseconds) + { + this.PeakPerformanceCounterEntry = entry; + } + } + + PerformanceCounter.Stopwatch.Stop(); + EventsLogged++; + } + + public PerformanceCounterEntry? GetPeak() + { + return this.PeakPerformanceCounterEntry; + } + + public void ResetPeak() + { + this.PeakPerformanceCounterEntry = null; + } + + public PerformanceCounterEntry? GetLastEntry() + { + if (this._counter.IsEmpty) + { + return null; + } + return this._counter.PeekLast(); + } + + public double GetAverage() + { + if (this._counter.IsEmpty) + { + return 0; + } + + return this._counter.Average(p => p.Elapsed.TotalMilliseconds); + } + + public double GetAverage(TimeSpan range) + { + if (this._counter.IsEmpty) + { + return 0; + } + + var lastTime = this._counter.Max(x => x.EventTime); + var start = lastTime.Subtract(range); + + var entries = this._counter.Where(x => (x.EventTime >= start) && (x.EventTime <= lastTime)); + return entries.Average(x => x.Elapsed.TotalMilliseconds); + } + } +} diff --git a/src/SMAPI/Framework/Utilities/PerformanceCounterEntry.cs b/src/SMAPI/Framework/Utilities/PerformanceCounterEntry.cs new file mode 100644 index 00000000..8e156a32 --- /dev/null +++ b/src/SMAPI/Framework/Utilities/PerformanceCounterEntry.cs @@ -0,0 +1,10 @@ +using System; + +namespace StardewModdingAPI.Framework.Utilities +{ + public struct PerformanceCounterEntry + { + public DateTime EventTime; + public TimeSpan Elapsed; + } +} diff --git a/src/SMAPI/SMAPI.csproj b/src/SMAPI/SMAPI.csproj index 3bb73295..5e407c2c 100644 --- a/src/SMAPI/SMAPI.csproj +++ b/src/SMAPI/SMAPI.csproj @@ -16,6 +16,7 @@ + -- cgit From 47f626cc99c93a28b2d6867ed6cc717b39ec062c Mon Sep 17 00:00:00 2001 From: Drachenkaetzchen Date: Fri, 10 Jan 2020 14:08:25 +0100 Subject: Moved most PerformanceCounter logic out of SCore into the new PerformanceCounterManager, some namespace refactoring --- src/SMAPI/Framework/Events/ManagedEvent.cs | 8 +- .../EventPerformanceCounterCategory.cs | 16 ++++ .../PerformanceCounter/IPerformanceCounterEvent.cs | 16 ++++ .../PerformanceCounter/PerformanceCounter.cs | 103 +++++++++++++++++++++ .../PerformanceCounter/PerformanceCounterEntry.cs | 10 ++ .../PerformanceCounterManager.cs | 82 ++++++++++++++++ src/SMAPI/Framework/SCore.cs | 83 ++--------------- .../Utilities/EventPerformanceCounterCategory.cs | 16 ---- .../Utilities/IPerformanceCounterEvent.cs | 16 ---- .../Framework/Utilities/PerformanceCounter.cs | 102 -------------------- .../Framework/Utilities/PerformanceCounterEntry.cs | 10 -- src/SMAPI/Program.cs | 1 + 12 files changed, 242 insertions(+), 221 deletions(-) create mode 100644 src/SMAPI/Framework/PerformanceCounter/EventPerformanceCounterCategory.cs create mode 100644 src/SMAPI/Framework/PerformanceCounter/IPerformanceCounterEvent.cs create mode 100644 src/SMAPI/Framework/PerformanceCounter/PerformanceCounter.cs create mode 100644 src/SMAPI/Framework/PerformanceCounter/PerformanceCounterEntry.cs create mode 100644 src/SMAPI/Framework/PerformanceCounter/PerformanceCounterManager.cs delete mode 100644 src/SMAPI/Framework/Utilities/EventPerformanceCounterCategory.cs delete mode 100644 src/SMAPI/Framework/Utilities/IPerformanceCounterEvent.cs delete mode 100644 src/SMAPI/Framework/Utilities/PerformanceCounter.cs delete mode 100644 src/SMAPI/Framework/Utilities/PerformanceCounterEntry.cs (limited to 'src') diff --git a/src/SMAPI/Framework/Events/ManagedEvent.cs b/src/SMAPI/Framework/Events/ManagedEvent.cs index 9a5cb174..bb915738 100644 --- a/src/SMAPI/Framework/Events/ManagedEvent.cs +++ b/src/SMAPI/Framework/Events/ManagedEvent.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; using StardewModdingAPI.Framework.Utilities; -using PerformanceCounter = StardewModdingAPI.Framework.Utilities.PerformanceCounter; +using PerformanceCounter = StardewModdingAPI.Framework.PerformanceCounter.PerformanceCounter; namespace StardewModdingAPI.Framework.Events { @@ -32,7 +32,7 @@ namespace StardewModdingAPI.Framework.Events /// The cached invocation list. private EventHandler[] CachedInvocationList; - public IDictionary PerformanceCounters { get; } = new Dictionary(); + public IDictionary PerformanceCounters { get; } = new Dictionary(); private readonly Stopwatch Stopwatch = new Stopwatch(); @@ -47,7 +47,7 @@ namespace StardewModdingAPI.Framework.Events public double GetGameAverageExecutionTime() { - if (this.PerformanceCounters.TryGetValue(Constants.GamePerformanceCounterName, out PerformanceCounter gameExecTime)) + if (this.PerformanceCounters.TryGetValue(Constants.GamePerformanceCounterName, out PerformanceCounter.PerformanceCounter gameExecTime)) { return gameExecTime.GetAverage(); } @@ -151,7 +151,7 @@ namespace StardewModdingAPI.Framework.Events if (!this.PerformanceCounters.ContainsKey(modName)) { - this.PerformanceCounters.Add(modName, new PerformanceCounter($"{modName}.{this.EventName}")); + this.PerformanceCounters.Add(modName, new PerformanceCounter.PerformanceCounter($"{modName}.{this.EventName}")); } this.PerformanceCounters[modName].Add(performanceCounterEntry); diff --git a/src/SMAPI/Framework/PerformanceCounter/EventPerformanceCounterCategory.cs b/src/SMAPI/Framework/PerformanceCounter/EventPerformanceCounterCategory.cs new file mode 100644 index 00000000..14f74317 --- /dev/null +++ b/src/SMAPI/Framework/PerformanceCounter/EventPerformanceCounterCategory.cs @@ -0,0 +1,16 @@ +namespace StardewModdingAPI.Framework.Utilities +{ + public class EventPerformanceCounterCategory + { + public IPerformanceCounterEvent Event { get; } + public double MonitorThreshold { get; } + public bool IsImportant { get; } + public bool Monitor { get; } + + public EventPerformanceCounterCategory(IPerformanceCounterEvent @event, bool isImportant) + { + this.Event = @event; + this.IsImportant = isImportant; + } + } +} diff --git a/src/SMAPI/Framework/PerformanceCounter/IPerformanceCounterEvent.cs b/src/SMAPI/Framework/PerformanceCounter/IPerformanceCounterEvent.cs new file mode 100644 index 00000000..6b83586d --- /dev/null +++ b/src/SMAPI/Framework/PerformanceCounter/IPerformanceCounterEvent.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; + +namespace StardewModdingAPI.Framework.Utilities +{ + public interface IPerformanceCounterEvent + { + string GetEventName(); + long GetAverageCallsPerSecond(); + IDictionary PerformanceCounters { get; } + + double GetGameAverageExecutionTime(); + double GetModsAverageExecutionTime(); + double GetAverageExecutionTime(); + } +} diff --git a/src/SMAPI/Framework/PerformanceCounter/PerformanceCounter.cs b/src/SMAPI/Framework/PerformanceCounter/PerformanceCounter.cs new file mode 100644 index 00000000..04e0f5f5 --- /dev/null +++ b/src/SMAPI/Framework/PerformanceCounter/PerformanceCounter.cs @@ -0,0 +1,103 @@ +using System; +using System.Diagnostics; +using System.Linq; +using Cyotek.Collections.Generic; +using StardewModdingAPI.Framework.Utilities; + +namespace StardewModdingAPI.Framework.PerformanceCounter +{ + public class PerformanceCounter + { + private const int MAX_ENTRIES = 16384; + + public string Name { get; } + public static Stopwatch Stopwatch = new Stopwatch(); + public static long TotalNumEventsLogged; + + + private readonly CircularBuffer _counter; + + private PerformanceCounterEntry? PeakPerformanceCounterEntry; + + public PerformanceCounter(string name) + { + this.Name = name; + this._counter = new CircularBuffer(PerformanceCounter.MAX_ENTRIES); + } + + public int GetAverageCallsPerSecond() + { + var x = this._counter.GroupBy( + p => + (int) p.EventTime.Subtract( + new DateTime(1970, 1, 1) + ).TotalSeconds); + + return x.Last().Count(); + } + + public void Add(PerformanceCounterEntry entry) + { + PerformanceCounter.Stopwatch.Start(); + this._counter.Put(entry); + + if (this.PeakPerformanceCounterEntry == null) + { + this.PeakPerformanceCounterEntry = entry; + } + else + { + if (entry.Elapsed.TotalMilliseconds > this.PeakPerformanceCounterEntry.Value.Elapsed.TotalMilliseconds) + { + this.PeakPerformanceCounterEntry = entry; + } + } + + PerformanceCounter.Stopwatch.Stop(); + PerformanceCounter.TotalNumEventsLogged++; + } + + public PerformanceCounterEntry? GetPeak() + { + return this.PeakPerformanceCounterEntry; + } + + public void ResetPeak() + { + this.PeakPerformanceCounterEntry = null; + } + + public PerformanceCounterEntry? GetLastEntry() + { + if (this._counter.IsEmpty) + { + return null; + } + return this._counter.PeekLast(); + } + + public double GetAverage() + { + if (this._counter.IsEmpty) + { + return 0; + } + + return this._counter.Average(p => p.Elapsed.TotalMilliseconds); + } + + public double GetAverage(TimeSpan range) + { + if (this._counter.IsEmpty) + { + return 0; + } + + var lastTime = this._counter.Max(x => x.EventTime); + var start = lastTime.Subtract(range); + + var entries = this._counter.Where(x => (x.EventTime >= start) && (x.EventTime <= lastTime)); + return entries.Average(x => x.Elapsed.TotalMilliseconds); + } + } +} diff --git a/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterEntry.cs b/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterEntry.cs new file mode 100644 index 00000000..8e156a32 --- /dev/null +++ b/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterEntry.cs @@ -0,0 +1,10 @@ +using System; + +namespace StardewModdingAPI.Framework.Utilities +{ + public struct PerformanceCounterEntry + { + public DateTime EventTime; + public TimeSpan Elapsed; + } +} diff --git a/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterManager.cs b/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterManager.cs new file mode 100644 index 00000000..e2200e74 --- /dev/null +++ b/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterManager.cs @@ -0,0 +1,82 @@ +using System.Collections.Generic; +using StardewModdingAPI.Framework.Events; +using StardewModdingAPI.Framework.Utilities; + +namespace StardewModdingAPI.Framework.PerformanceCounter +{ + internal class PerformanceCounterManager + { + public HashSet PerformanceCounterEvents = new HashSet(); + + private readonly EventManager EventManager; + + public PerformanceCounterManager(EventManager eventManager) + { + this.EventManager = eventManager; + this.InitializePerformanceCounterEvents(); + } + + private void InitializePerformanceCounterEvents() + { + this.PerformanceCounterEvents = new HashSet() + { + new EventPerformanceCounterCategory(this.EventManager.MenuChanged, false), + + // Rendering Events + new EventPerformanceCounterCategory(this.EventManager.Rendering, true), + new EventPerformanceCounterCategory(this.EventManager.Rendered, true), + new EventPerformanceCounterCategory(this.EventManager.RenderingWorld, true), + new EventPerformanceCounterCategory(this.EventManager.RenderedWorld, true), + new EventPerformanceCounterCategory(this.EventManager.RenderingActiveMenu, true), + new EventPerformanceCounterCategory(this.EventManager.RenderedActiveMenu, true), + new EventPerformanceCounterCategory(this.EventManager.RenderingHud, true), + new EventPerformanceCounterCategory(this.EventManager.RenderedHud, true), + + new EventPerformanceCounterCategory(this.EventManager.WindowResized, false), + new EventPerformanceCounterCategory(this.EventManager.GameLaunched, false), + new EventPerformanceCounterCategory(this.EventManager.UpdateTicking, true), + new EventPerformanceCounterCategory(this.EventManager.UpdateTicked, true), + new EventPerformanceCounterCategory(this.EventManager.OneSecondUpdateTicking, true), + new EventPerformanceCounterCategory(this.EventManager.OneSecondUpdateTicked, true), + + new EventPerformanceCounterCategory(this.EventManager.SaveCreating, false), + new EventPerformanceCounterCategory(this.EventManager.SaveCreated, false), + new EventPerformanceCounterCategory(this.EventManager.Saving, false), + new EventPerformanceCounterCategory(this.EventManager.Saved, false), + + new EventPerformanceCounterCategory(this.EventManager.DayStarted, false), + new EventPerformanceCounterCategory(this.EventManager.DayEnding, false), + + new EventPerformanceCounterCategory(this.EventManager.TimeChanged, true), + + new EventPerformanceCounterCategory(this.EventManager.ReturnedToTitle, false), + + new EventPerformanceCounterCategory(this.EventManager.ButtonPressed, true), + new EventPerformanceCounterCategory(this.EventManager.ButtonReleased, true), + new EventPerformanceCounterCategory(this.EventManager.CursorMoved, true), + new EventPerformanceCounterCategory(this.EventManager.MouseWheelScrolled, true), + + new EventPerformanceCounterCategory(this.EventManager.PeerContextReceived, true), + new EventPerformanceCounterCategory(this.EventManager.ModMessageReceived, true), + new EventPerformanceCounterCategory(this.EventManager.PeerDisconnected, true), + new EventPerformanceCounterCategory(this.EventManager.InventoryChanged, true), + new EventPerformanceCounterCategory(this.EventManager.LevelChanged, true), + new EventPerformanceCounterCategory(this.EventManager.Warped, true), + + new EventPerformanceCounterCategory(this.EventManager.LocationListChanged, true), + new EventPerformanceCounterCategory(this.EventManager.BuildingListChanged, true), + new EventPerformanceCounterCategory(this.EventManager.LocationListChanged, true), + new EventPerformanceCounterCategory(this.EventManager.DebrisListChanged, true), + new EventPerformanceCounterCategory(this.EventManager.LargeTerrainFeatureListChanged, true), + new EventPerformanceCounterCategory(this.EventManager.NpcListChanged, true), + new EventPerformanceCounterCategory(this.EventManager.ObjectListChanged, true), + new EventPerformanceCounterCategory(this.EventManager.ChestInventoryChanged, true), + new EventPerformanceCounterCategory(this.EventManager.TerrainFeatureListChanged, true), + new EventPerformanceCounterCategory(this.EventManager.LoadStageChanged, false), + new EventPerformanceCounterCategory(this.EventManager.UnvalidatedUpdateTicking, true), + new EventPerformanceCounterCategory(this.EventManager.UnvalidatedUpdateTicked, true), + + }; + } + } +} diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index 6946a817..74a256fe 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -34,7 +34,7 @@ using StardewModdingAPI.Toolkit.Serialization; using StardewModdingAPI.Toolkit.Utilities; using StardewValley; using Object = StardewValley.Object; -using PerformanceCounter = StardewModdingAPI.Framework.Utilities.PerformanceCounter; +using PerformanceCounterManager = StardewModdingAPI.Framework.PerformanceCounter.PerformanceCounterManager; using ThreadState = System.Threading.ThreadState; namespace StardewModdingAPI.Framework @@ -79,11 +79,11 @@ namespace StardewModdingAPI.Framework /// This is initialized after the game starts. private readonly ModRegistry ModRegistry = new ModRegistry(); - private HashSet PerformanceCounterEvents = new HashSet(); - /// Manages SMAPI events for mods. private readonly EventManager EventManager; + private readonly PerformanceCounterManager PerformanceCounterManager; + /// Whether the game is currently running. private bool IsGameRunning; @@ -166,7 +166,7 @@ namespace StardewModdingAPI.Framework }; this.MonitorForGame = this.GetSecondaryMonitor("game"); this.EventManager = new EventManager(this.Monitor, this.ModRegistry); - this.InitializePerformanceCounterEvents(); + this.PerformanceCounterManager = new PerformanceCounterManager(this.EventManager); SCore.DeprecationManager = new DeprecationManager(this.Monitor, this.ModRegistry); @@ -206,69 +206,6 @@ namespace StardewModdingAPI.Framework #endif } - private void InitializePerformanceCounterEvents() - { - this.PerformanceCounterEvents = new HashSet() - { - new EventPerformanceCounterCategory(this.EventManager.MenuChanged, false), - - // Rendering Events - new EventPerformanceCounterCategory(this.EventManager.Rendering, true), - new EventPerformanceCounterCategory(this.EventManager.Rendered, true), - new EventPerformanceCounterCategory(this.EventManager.RenderingWorld, true), - new EventPerformanceCounterCategory(this.EventManager.RenderedWorld, true), - new EventPerformanceCounterCategory(this.EventManager.RenderingActiveMenu, true), - new EventPerformanceCounterCategory(this.EventManager.RenderedActiveMenu, true), - new EventPerformanceCounterCategory(this.EventManager.RenderingHud, true), - new EventPerformanceCounterCategory(this.EventManager.RenderedHud, true), - - new EventPerformanceCounterCategory(this.EventManager.WindowResized, false), - new EventPerformanceCounterCategory(this.EventManager.GameLaunched, false), - new EventPerformanceCounterCategory(this.EventManager.UpdateTicking, true), - new EventPerformanceCounterCategory(this.EventManager.UpdateTicked, true), - new EventPerformanceCounterCategory(this.EventManager.OneSecondUpdateTicking, true), - new EventPerformanceCounterCategory(this.EventManager.OneSecondUpdateTicked, true), - - new EventPerformanceCounterCategory(this.EventManager.SaveCreating, false), - new EventPerformanceCounterCategory(this.EventManager.SaveCreated, false), - new EventPerformanceCounterCategory(this.EventManager.Saving, false), - new EventPerformanceCounterCategory(this.EventManager.Saved, false), - - new EventPerformanceCounterCategory(this.EventManager.DayStarted, false), - new EventPerformanceCounterCategory(this.EventManager.DayEnding, false), - - new EventPerformanceCounterCategory(this.EventManager.TimeChanged, true), - - new EventPerformanceCounterCategory(this.EventManager.ReturnedToTitle, false), - - new EventPerformanceCounterCategory(this.EventManager.ButtonPressed, true), - new EventPerformanceCounterCategory(this.EventManager.ButtonReleased, true), - new EventPerformanceCounterCategory(this.EventManager.CursorMoved, true), - new EventPerformanceCounterCategory(this.EventManager.MouseWheelScrolled, true), - - new EventPerformanceCounterCategory(this.EventManager.PeerContextReceived, true), - new EventPerformanceCounterCategory(this.EventManager.ModMessageReceived, true), - new EventPerformanceCounterCategory(this.EventManager.PeerDisconnected, true), - new EventPerformanceCounterCategory(this.EventManager.InventoryChanged, true), - new EventPerformanceCounterCategory(this.EventManager.LevelChanged, true), - new EventPerformanceCounterCategory(this.EventManager.Warped, true), - - new EventPerformanceCounterCategory(this.EventManager.LocationListChanged, true), - new EventPerformanceCounterCategory(this.EventManager.BuildingListChanged, true), - new EventPerformanceCounterCategory(this.EventManager.LocationListChanged, true), - new EventPerformanceCounterCategory(this.EventManager.DebrisListChanged, true), - new EventPerformanceCounterCategory(this.EventManager.LargeTerrainFeatureListChanged, true), - new EventPerformanceCounterCategory(this.EventManager.NpcListChanged, true), - new EventPerformanceCounterCategory(this.EventManager.ObjectListChanged, true), - new EventPerformanceCounterCategory(this.EventManager.ChestInventoryChanged, true), - new EventPerformanceCounterCategory(this.EventManager.TerrainFeatureListChanged, true), - new EventPerformanceCounterCategory(this.EventManager.LoadStageChanged, false), - new EventPerformanceCounterCategory(this.EventManager.UnvalidatedUpdateTicking, true), - new EventPerformanceCounterCategory(this.EventManager.UnvalidatedUpdateTicked, true), - - }; - } - /// Launch SMAPI. [HandleProcessCorruptedStateExceptions, SecurityCritical] // let try..catch handle corrupted state exceptions public void RunInteractively() @@ -1471,7 +1408,7 @@ namespace StardewModdingAPI.Framework } else { - var data = this.PerformanceCounterEvents.Where(p => p.Event.GetEventName().ToLowerInvariant().Contains(filterByName.ToLowerInvariant())); + var data = this.PerformanceCounterManager.PerformanceCounterEvents.Where(p => p.Event.GetEventName().ToLowerInvariant().Contains(filterByName.ToLowerInvariant())); foreach (var i in data) { @@ -1479,8 +1416,8 @@ namespace StardewModdingAPI.Framework } } - double avgTime = PerformanceCounter.Stopwatch.ElapsedMilliseconds / (double)PerformanceCounter.EventsLogged; - this.Monitor.Log($"Logged {PerformanceCounter.EventsLogged} events in {PerformanceCounter.Stopwatch.ElapsedMilliseconds}ms (avg {avgTime:F4}ms / event)"); + double avgTime = PerformanceCounter.PerformanceCounter.Stopwatch.ElapsedMilliseconds / (double)PerformanceCounter.PerformanceCounter.TotalNumEventsLogged; + this.Monitor.Log($"Logged {PerformanceCounter.PerformanceCounter.TotalNumEventsLogged} events in {PerformanceCounter.PerformanceCounter.Stopwatch.ElapsedMilliseconds}ms (avg {avgTime:F4}ms / event)"); } @@ -1492,17 +1429,17 @@ namespace StardewModdingAPI.Framework if (eventNameFilter != null) { - data = this.PerformanceCounterEvents.Where(p => p.Event.GetEventName().ToLowerInvariant().Contains(eventNameFilter.ToLowerInvariant())); + data = this.PerformanceCounterManager.PerformanceCounterEvents.Where(p => p.Event.GetEventName().ToLowerInvariant().Contains(eventNameFilter.ToLowerInvariant())); } else { if (showOnlyImportant) { - data = this.PerformanceCounterEvents.Where(p => p.IsImportant); + data = this.PerformanceCounterManager.PerformanceCounterEvents.Where(p => p.IsImportant); } else { - data = this.PerformanceCounterEvents; + data = this.PerformanceCounterManager.PerformanceCounterEvents; } } diff --git a/src/SMAPI/Framework/Utilities/EventPerformanceCounterCategory.cs b/src/SMAPI/Framework/Utilities/EventPerformanceCounterCategory.cs deleted file mode 100644 index 14f74317..00000000 --- a/src/SMAPI/Framework/Utilities/EventPerformanceCounterCategory.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace StardewModdingAPI.Framework.Utilities -{ - public class EventPerformanceCounterCategory - { - public IPerformanceCounterEvent Event { get; } - public double MonitorThreshold { get; } - public bool IsImportant { get; } - public bool Monitor { get; } - - public EventPerformanceCounterCategory(IPerformanceCounterEvent @event, bool isImportant) - { - this.Event = @event; - this.IsImportant = isImportant; - } - } -} diff --git a/src/SMAPI/Framework/Utilities/IPerformanceCounterEvent.cs b/src/SMAPI/Framework/Utilities/IPerformanceCounterEvent.cs deleted file mode 100644 index 55302f90..00000000 --- a/src/SMAPI/Framework/Utilities/IPerformanceCounterEvent.cs +++ /dev/null @@ -1,16 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace StardewModdingAPI.Framework.Utilities -{ - public interface IPerformanceCounterEvent - { - string GetEventName(); - long GetAverageCallsPerSecond(); - IDictionary PerformanceCounters { get; } - - double GetGameAverageExecutionTime(); - double GetModsAverageExecutionTime(); - double GetAverageExecutionTime(); - } -} diff --git a/src/SMAPI/Framework/Utilities/PerformanceCounter.cs b/src/SMAPI/Framework/Utilities/PerformanceCounter.cs deleted file mode 100644 index c9ffcf5b..00000000 --- a/src/SMAPI/Framework/Utilities/PerformanceCounter.cs +++ /dev/null @@ -1,102 +0,0 @@ -using System; -using System.Diagnostics; -using System.Linq; -using Cyotek.Collections.Generic; - -namespace StardewModdingAPI.Framework.Utilities -{ - public class PerformanceCounter - { - private const int MaxCount = 16384; - - public string Name { get; } - public static Stopwatch Stopwatch = new Stopwatch(); - public static long EventsLogged; - - - private readonly CircularBuffer _counter; - - private PerformanceCounterEntry? PeakPerformanceCounterEntry; - - public PerformanceCounter(string name) - { - this.Name = name; - this._counter = new CircularBuffer(PerformanceCounter.MaxCount); - } - - public int GetAverageCallsPerSecond() - { - var x = this._counter.GroupBy( - p => - (int) p.EventTime.Subtract( - new DateTime(1970, 1, 1) - ).TotalSeconds); - - return x.Last().Count(); - } - - public void Add(PerformanceCounterEntry entry) - { - PerformanceCounter.Stopwatch.Start(); - this._counter.Put(entry); - - if (this.PeakPerformanceCounterEntry == null) - { - this.PeakPerformanceCounterEntry = entry; - } - else - { - if (entry.Elapsed.TotalMilliseconds > this.PeakPerformanceCounterEntry.Value.Elapsed.TotalMilliseconds) - { - this.PeakPerformanceCounterEntry = entry; - } - } - - PerformanceCounter.Stopwatch.Stop(); - EventsLogged++; - } - - public PerformanceCounterEntry? GetPeak() - { - return this.PeakPerformanceCounterEntry; - } - - public void ResetPeak() - { - this.PeakPerformanceCounterEntry = null; - } - - public PerformanceCounterEntry? GetLastEntry() - { - if (this._counter.IsEmpty) - { - return null; - } - return this._counter.PeekLast(); - } - - public double GetAverage() - { - if (this._counter.IsEmpty) - { - return 0; - } - - return this._counter.Average(p => p.Elapsed.TotalMilliseconds); - } - - public double GetAverage(TimeSpan range) - { - if (this._counter.IsEmpty) - { - return 0; - } - - var lastTime = this._counter.Max(x => x.EventTime); - var start = lastTime.Subtract(range); - - var entries = this._counter.Where(x => (x.EventTime >= start) && (x.EventTime <= lastTime)); - return entries.Average(x => x.Elapsed.TotalMilliseconds); - } - } -} diff --git a/src/SMAPI/Framework/Utilities/PerformanceCounterEntry.cs b/src/SMAPI/Framework/Utilities/PerformanceCounterEntry.cs deleted file mode 100644 index 8e156a32..00000000 --- a/src/SMAPI/Framework/Utilities/PerformanceCounterEntry.cs +++ /dev/null @@ -1,10 +0,0 @@ -using System; - -namespace StardewModdingAPI.Framework.Utilities -{ - public struct PerformanceCounterEntry - { - public DateTime EventTime; - public TimeSpan Elapsed; - } -} diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index 6bacf564..b5e6307a 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -11,6 +11,7 @@ using StardewModdingAPI.Framework; using StardewModdingAPI.Toolkit.Utilities; [assembly: InternalsVisibleTo("SMAPI.Tests")] +[assembly: InternalsVisibleTo("SMAPI.Mods.ConsoleCommands")] [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] // Moq for unit testing namespace StardewModdingAPI { -- cgit From 8a77373b18dbda77f268e8e7f772e950da60829f Mon Sep 17 00:00:00 2001 From: Drachenkaetzchen Date: Fri, 10 Jan 2020 14:16:00 +0100 Subject: Added reset functionality --- .../Framework/PerformanceCounter/PerformanceCounter.cs | 6 ++++++ .../PerformanceCounter/PerformanceCounterManager.cs | 16 ++++++++++++++++ src/SMAPI/Framework/SCore.cs | 8 ++++++-- 3 files changed, 28 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/src/SMAPI/Framework/PerformanceCounter/PerformanceCounter.cs b/src/SMAPI/Framework/PerformanceCounter/PerformanceCounter.cs index 04e0f5f5..0b0275b7 100644 --- a/src/SMAPI/Framework/PerformanceCounter/PerformanceCounter.cs +++ b/src/SMAPI/Framework/PerformanceCounter/PerformanceCounter.cs @@ -25,6 +25,12 @@ namespace StardewModdingAPI.Framework.PerformanceCounter this._counter = new CircularBuffer(PerformanceCounter.MAX_ENTRIES); } + public void Reset() + { + this._counter.Clear(); + this.PeakPerformanceCounterEntry = null; + } + public int GetAverageCallsPerSecond() { var x = this._counter.GroupBy( diff --git a/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterManager.cs b/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterManager.cs index e2200e74..ae7258e2 100644 --- a/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterManager.cs +++ b/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterManager.cs @@ -16,6 +16,22 @@ namespace StardewModdingAPI.Framework.PerformanceCounter this.InitializePerformanceCounterEvents(); } + public void Reset() + { + foreach (var performanceCounter in this.PerformanceCounterEvents) + { + this.ResetCategory(performanceCounter); + } + } + + public void ResetCategory(EventPerformanceCounterCategory category) + { + foreach (var eventPerformanceCounter in category.Event.PerformanceCounters) + { + eventPerformanceCounter.Value.Reset(); + } + } + private void InitializePerformanceCounterEvents() { this.PerformanceCounterEvents = new HashSet() diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index 74a256fe..d1dba9ea 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -498,8 +498,9 @@ namespace StardewModdingAPI.Framework "Usage: performance_counters [name] [threshold]\n"+ "Shows detailed performance counters for a specific event\n"+ "- name: The (partial) name of the event\n"+ - "- threshold: The minimum avg execution time (ms) of the event\n"+ - "", this.HandleCommand); + "- threshold: The minimum avg execution time (ms) of the event\n\n"+ + "Usage: performance_counters reset\n"+ + "Resets all performance counters\n", this.HandleCommand); this.GameInstance.CommandManager.Add(null, "pc", "Alias for performance_counters", this.HandleCommand); // start handling command line input @@ -1393,6 +1394,9 @@ namespace StardewModdingAPI.Framework } } break; + case "reset": + this.PerformanceCounterManager.Reset(); + return; default: showSummary = false; filterByName = arguments[0]; -- cgit From 280dc911839f8996cddd9804f3f545cc38d20243 Mon Sep 17 00:00:00 2001 From: Drachenkaetzchen Date: Sat, 11 Jan 2020 15:45:45 +0100 Subject: Reworked the console implementation, added monitoring. Some internal refactoring. --- .../Framework/Commands/ArgumentParser.cs | 63 +++ .../Commands/Other/PerformanceCounterCommand.cs | 533 ++++++++++++++++++++- .../SMAPI.Mods.ConsoleCommands.csproj | 4 + src/SMAPI/Constants.cs | 2 +- src/SMAPI/Framework/Events/EventManager.cs | 5 +- src/SMAPI/Framework/Events/IManagedEvent.cs | 7 + src/SMAPI/Framework/Events/ManagedEvent.cs | 103 ++-- .../Framework/PerformanceCounter/AlertContext.cs | 14 + .../Framework/PerformanceCounter/AlertEntry.cs | 20 + .../EventPerformanceCounterCategory.cs | 16 - .../EventPerformanceCounterCollection.cs | 11 + .../PerformanceCounter/IPerformanceCounterEvent.cs | 1 - .../PerformanceCounter/PerformanceCounter.cs | 18 +- .../PerformanceCounterCollection.cs | 144 ++++++ .../PerformanceCounterManager.cs | 233 ++++++--- src/SMAPI/Framework/SCore.cs | 193 +------- src/SMAPI/Framework/SGame.cs | 10 +- src/SMAPI/Program.cs | 2 +- 18 files changed, 1027 insertions(+), 352 deletions(-) create mode 100644 src/SMAPI/Framework/Events/IManagedEvent.cs create mode 100644 src/SMAPI/Framework/PerformanceCounter/AlertContext.cs create mode 100644 src/SMAPI/Framework/PerformanceCounter/AlertEntry.cs delete mode 100644 src/SMAPI/Framework/PerformanceCounter/EventPerformanceCounterCategory.cs create mode 100644 src/SMAPI/Framework/PerformanceCounter/EventPerformanceCounterCollection.cs create mode 100644 src/SMAPI/Framework/PerformanceCounter/PerformanceCounterCollection.cs (limited to 'src') diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/ArgumentParser.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/ArgumentParser.cs index 10007b42..40691a3e 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/ArgumentParser.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/ArgumentParser.cs @@ -1,6 +1,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Globalization; using System.Linq; namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands @@ -113,6 +114,51 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands return true; } + public bool IsDecimal(int index) + { + if (!this.TryGet(index, "", out string raw, false)) + return false; + + if (!decimal.TryParse(raw, NumberStyles.Number, CultureInfo.InvariantCulture, out decimal value)) + { + return false; + } + + return true; + } + + /// Try to read a decimal argument. + /// The argument index. + /// The argument name for error messages. + /// The parsed value. + /// Whether to show an error if the argument is missing. + /// The minimum value allowed. + /// The maximum value allowed. + public bool TryGetDecimal(int index, string name, out decimal value, bool required = true, decimal? min = null, decimal? max = null) + { + value = 0; + + // get argument + if (!this.TryGet(index, name, out string raw, required)) + return false; + + // parse + if (!decimal.TryParse(raw, NumberStyles.Number, CultureInfo.InvariantCulture, out value)) + { + this.LogDecimalFormatError(index, name, min, max); + return false; + } + + // validate + if ((min.HasValue && value < min) || (max.HasValue && value > max)) + { + this.LogDecimalFormatError(index, name, min, max); + return false; + } + + return true; + } + /// Returns an enumerator that iterates through the collection. /// An enumerator that can be used to iterate through the collection. public IEnumerator GetEnumerator() @@ -154,5 +200,22 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands else this.LogError($"Argument {index} ({name}) must be an integer."); } + + /// Print an error for an invalid decimal argument. + /// The argument index. + /// The argument name for error messages. + /// The minimum value allowed. + /// The maximum value allowed. + private void LogDecimalFormatError(int index, string name, decimal? min, decimal? max) + { + if (min.HasValue && max.HasValue) + this.LogError($"Argument {index} ({name}) must be a decimal between {min} and {max}."); + else if (min.HasValue) + this.LogError($"Argument {index} ({name}) must be a decimal and at least {min}."); + else if (max.HasValue) + this.LogError($"Argument {index} ({name}) must be a decimal and at most {max}."); + else + this.LogError($"Argument {index} ({name}) must be a decimal."); + } } } diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/PerformanceCounterCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/PerformanceCounterCommand.cs index b7e56359..84b9504e 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/PerformanceCounterCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/PerformanceCounterCommand.cs @@ -1,14 +1,543 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using StardewModdingAPI.Framework; +using StardewModdingAPI.Framework.PerformanceCounter; + namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other { - internal class PerformanceCounterCommand: TrainerCommand + internal class PerformanceCounterCommand : TrainerCommand { - public PerformanceCounterCommand(string name, string description) : base("performance_counters", "Displays performance counters") + private readonly Dictionary CommandNames = new Dictionary() + { + {Command.Summary, new[] {"summary", "sum", "s"}}, + {Command.Detail, new[] {"detail", "d"}}, + {Command.Reset, new[] {"reset", "r"}}, + {Command.Monitor, new[] {"monitor"}}, + {Command.Examples, new[] {"examples"}}, + {Command.Concepts, new[] {"concepts"}}, + {Command.Help, new[] {"help"}}, + }; + + private enum Command + { + Summary, + Detail, + Reset, + Monitor, + Examples, + Help, + Concepts, + None + } + + public PerformanceCounterCommand() : base("pc", PerformanceCounterCommand.GetDescription()) { } public override void Handle(IMonitor monitor, string command, ArgumentParser args) { + if (args.TryGet(0, "command", out string subCommandString, false)) + { + Command subCommand = this.ParseCommandString(subCommandString); + + switch (subCommand) + { + case Command.Summary: + this.DisplayPerformanceCounterSummary(monitor, args); + break; + case Command.Detail: + this.DisplayPerformanceCounterDetail(monitor, args); + break; + case Command.Reset: + this.ResetCounter(monitor, args); + break; + case Command.Monitor: + this.HandleMonitor(monitor, args); + break; + case Command.Examples: + break; + case Command.Concepts: + this.ShowHelp(monitor, Command.Concepts); + break; + case Command.Help: + args.TryGet(1, "command", out string commandString, true); + + var helpCommand = this.ParseCommandString(commandString); + this.ShowHelp(monitor, helpCommand); + break; + default: + this.LogUsageError(monitor, $"Unknown command {subCommandString}"); + break; + } + } + else + { + this.DisplayPerformanceCounterSummary(monitor, args); + } + } + + private Command ParseCommandString(string command) + { + foreach (var i in this.CommandNames.Where(i => i.Value.Any(str => str.Equals(command, StringComparison.InvariantCultureIgnoreCase)))) + { + return i.Key; + } + + return Command.None; + } + + private void HandleMonitor(IMonitor monitor, ArgumentParser args) + { + if (args.TryGet(1, "mode", out string mode, false)) + { + switch (mode) + { + case "list": + this.ListMonitors(monitor); + break; + case "collection": + args.TryGet(2, "name", out string collectionName); + decimal threshold = 0; + if (args.IsDecimal(3) && args.TryGetDecimal(3, "threshold", out threshold, false)) + { + this.SetCollectionMonitor(monitor, collectionName, null, (double)threshold); + } else if (args.TryGet(3, "source", out string source)) + { + if (args.TryGetDecimal(4, "threshold", out threshold)) + { + this.SetCollectionMonitor(monitor, collectionName, source, (double) threshold); + } + } + break; + case "clear": + this.ClearMonitors(monitor); + break; + default: + monitor.Log($"Unknown mode {mode}. See 'pc help monitor' for usage."); + break; + } + + } + else + { + this.ListMonitors(monitor); + } + } + + private void SetCollectionMonitor(IMonitor monitor, string collectionName, string sourceName, double threshold) + { + foreach (PerformanceCounterCollection collection in SCore.PerformanceCounterManager.PerformanceCounterCollections) + { + if (collection.Name.ToLowerInvariant().Equals(collectionName.ToLowerInvariant())) + { + if (sourceName == null) + { + collection.Monitor = true; + collection.MonitorThresholdMilliseconds = threshold; + monitor.Log($"Set up monitor for '{collectionName}' with '{this.FormatMilliseconds(threshold)}'", LogLevel.Info); + return; + } + else + { + foreach (var performanceCounter in collection.PerformanceCounters) + { + if (performanceCounter.Value.Source.ToLowerInvariant().Equals(sourceName.ToLowerInvariant())) + { + performanceCounter.Value.Monitor = true; + performanceCounter.Value.MonitorThresholdMilliseconds = threshold; + monitor.Log($"Set up monitor for '{sourceName}' in collection '{collectionName}' with '{this.FormatMilliseconds(threshold)}", LogLevel.Info); + return; + } + } + + monitor.Log($"Could not find the source '{sourceName}' in collection '{collectionName}'", LogLevel.Warn); + return; + } + } + } + + monitor.Log($"Could not find the collection '{collectionName}'", LogLevel.Warn); + } + + + private void ClearMonitors(IMonitor monitor) + { + int clearedCounters = 0; + foreach (PerformanceCounterCollection collection in SCore.PerformanceCounterManager.PerformanceCounterCollections) + { + if (collection.Monitor) + { + collection.Monitor = false; + clearedCounters++; + } + + foreach (var performanceCounter in collection.PerformanceCounters) + { + if (performanceCounter.Value.Monitor) + { + performanceCounter.Value.Monitor = false; + clearedCounters++; + } + } + + } + + monitor.Log($"Cleared {clearedCounters} counters.", LogLevel.Info); + } + + private void ListMonitors(IMonitor monitor) + { + StringBuilder sb = new StringBuilder(); + sb.AppendLine(); + sb.AppendLine(); + var collectionMonitors = new List<(string collectionName, double threshold)>(); + var sourceMonitors = new List<(string collectionName, string sourceName, double threshold)>(); + + foreach (PerformanceCounterCollection collection in SCore.PerformanceCounterManager.PerformanceCounterCollections) + { + if (collection.Monitor) + { + collectionMonitors.Add((collection.Name, collection.MonitorThresholdMilliseconds)); + } + + sourceMonitors.AddRange(from performanceCounter in + collection.PerformanceCounters where performanceCounter.Value.Monitor + select (collection.Name, performanceCounter.Value.Source, performanceCounter.Value.MonitorThresholdMilliseconds)); + } + + if (collectionMonitors.Count > 0) + { + sb.AppendLine("Collection Monitors:"); + sb.AppendLine(); + sb.AppendLine(this.GetTableString( + data: collectionMonitors, + header: new[] {"Collection", "Threshold"}, + getRow: item => new[] + { + item.collectionName, + this.FormatMilliseconds(item.threshold) + } + )); + + sb.AppendLine(); + + + } + + if (sourceMonitors.Count > 0) + { + sb.AppendLine("Source Monitors:"); + sb.AppendLine(); + sb.AppendLine(this.GetTableString( + data: sourceMonitors, + header: new[] {"Collection", "Source", "Threshold"}, + getRow: item => new[] + { + item.collectionName, + item.sourceName, + this.FormatMilliseconds(item.threshold) + } + )); + + sb.AppendLine(); + } + + monitor.Log(sb.ToString(), LogLevel.Info); + } + + private void ShowHelp(IMonitor monitor, Command command) + { + StringBuilder sb = new StringBuilder(); + sb.AppendLine(); + switch (command) + { + case Command.Concepts: + sb.AppendLine("A performance counter is a metric which measures execution time. Each performance"); + sb.AppendLine("counter consists of:"); + sb.AppendLine(); + sb.AppendLine(" - A source, which typically is a mod or the game itself."); + sb.AppendLine(" - A ring buffer which stores the data points (execution time and time when it was executed)"); + sb.AppendLine(); + sb.AppendLine("A set of performance counters is organized in a collection to group various areas."); + sb.AppendLine("Per default, collections for all game events [1] are created."); + sb.AppendLine(); + sb.AppendLine("Example:"); + sb.AppendLine(); + sb.AppendLine("The performance counter collection named 'Display.Rendered' contains one performance"); + sb.AppendLine("counters when the game executes the 'Display.Rendered' event, and one additional"); + sb.AppendLine("performance counter for each mod which handles the 'Display.Rendered' event."); + sb.AppendLine(); + sb.AppendLine("[1] https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Events"); + break; + case Command.Detail: + sb.AppendLine("Usage: pc detail "); + sb.AppendLine(" pc detail "); + sb.AppendLine(); + sb.AppendLine("Displays details for a specific collection."); + sb.AppendLine(); + sb.AppendLine("Arguments:"); + sb.AppendLine(" Required. The full or partial name of the collection to display."); + sb.AppendLine(" Optional. The full or partial name of the source."); + sb.AppendLine(" Optional. The threshold in milliseconds. Any average execution time below that"); + sb.AppendLine(" threshold is not reported."); + sb.AppendLine(); + sb.AppendLine("Examples:"); + sb.AppendLine("pc detail Display.Rendering Displays all performance counters for the 'Display.Rendering' collection"); + sb.AppendLine("pc detail Display.Rendering Pathoschild.ChestsAnywhere Displays the 'Display.Rendering' performance counter for 'Pathoschild.ChestsAnywhere'"); + sb.AppendLine("pc detail Display.Rendering 5 Displays the 'Display.Rendering' performance counters exceeding an average of 5ms"); + break; + case Command.Summary: + sb.AppendLine("Usage: pc summary "); + sb.AppendLine(); + sb.AppendLine("Displays the performance counter summary."); + sb.AppendLine(); + sb.AppendLine("Arguments:"); + sb.AppendLine(" Optional. Defaults to 'important' if omitted. Specifies one of these modes:"); + sb.AppendLine(" - all Displays performance counters from all collections"); + sb.AppendLine(" - important Displays only important performance counter collections"); + sb.AppendLine(); + sb.AppendLine(" Optional. Only shows performance counter collections matching the given name"); + sb.AppendLine(); + sb.AppendLine("Examples:"); + sb.AppendLine("pc summary all Shows all events"); + sb.AppendLine("pc summary Display.Rendering Shows only the 'Display.Rendering' collection"); + break; + case Command.Monitor: + sb.AppendLine("Usage: pc monitor "); + sb.AppendLine("Usage: pc monitor "); + sb.AppendLine(); + sb.AppendLine("Manages monitoring settings."); + sb.AppendLine(); + sb.AppendLine("Arguments:"); + sb.AppendLine(" Optional. Specifies if a specific source or a specific collection should be monitored."); + sb.AppendLine(" - list Lists current monitoring settings"); + sb.AppendLine(" - collection Sets up a monitor for a collection"); + sb.AppendLine(" - clear Clears all monitoring entries"); + sb.AppendLine(" Defaults to 'list' if not specified."); + sb.AppendLine(); + sb.AppendLine(" Required if the mode 'collection' is specified."); + sb.AppendLine(" Specifies the name of the collection to be monitored. Must be an exact match."); + sb.AppendLine(); + sb.AppendLine(" Optional. Specifies the name of a specific source. Must be an exact match."); + sb.AppendLine(); + sb.AppendLine(" Required if the mode 'collection' is specified."); + sb.AppendLine(" Specifies the threshold in milliseconds (fractions allowed)."); + sb.AppendLine(" Can also be 'remove' to remove the threshold."); + sb.AppendLine(); + sb.AppendLine("Examples:"); + sb.AppendLine(); + sb.AppendLine("pc monitor collection Display.Rendering 10"); + sb.AppendLine(" Sets up monitoring to write an alert on the console if the execution time of all performance counters in"); + sb.AppendLine(" the 'Display.Rendering' collection exceed 10 milliseconds."); + sb.AppendLine(); + sb.AppendLine("pc monitor collection Display.Rendering Pathoschild.ChestsAnywhere 5"); + sb.AppendLine(" Sets up monitoring to write an alert on the console if the execution time of Pathoschild.ChestsAnywhere in"); + sb.AppendLine(" the 'Display.Rendering' collection exceed 5 milliseconds."); + sb.AppendLine(); + sb.AppendLine("pc monitor collection Display.Rendering remove"); + sb.AppendLine(" Removes the threshold previously defined from the collection. Note that source-specific thresholds are left intact."); + sb.AppendLine(); + sb.AppendLine("pc monitor clear"); + sb.AppendLine(" Clears all previously setup monitors."); + break; + case Command.Reset: + sb.AppendLine("Usage: pc reset "); + sb.AppendLine(); + sb.AppendLine("Resets performance counters."); + sb.AppendLine(); + sb.AppendLine("Arguments:"); + sb.AppendLine(" Optional. Specifies if a collection or source should be reset."); + sb.AppendLine(" If omitted, all performance counters are reset."); + sb.AppendLine(); + sb.AppendLine(" - source Clears performance counters for a specific source"); + sb.AppendLine(" - collection Clears performance counters for a specific collection"); + sb.AppendLine(); + sb.AppendLine(" Required if a is given. Specifies the name of either the collection"); + sb.AppendLine(" or the source. The name must be an exact match."); + sb.AppendLine(); + sb.AppendLine("Examples:"); + sb.AppendLine("pc reset Resets all performance counters"); + sb.AppendLine("pc reset source Pathoschild.ChestsAnywhere Resets all performance for the source named Pathoschild.ChestsAnywhere"); + sb.AppendLine("pc reset collection Display.Rendering Resets all performance for the collection named Display.Rendering"); + break; + } + + sb.AppendLine(); + monitor.Log(sb.ToString(), LogLevel.Info); + } + + private void ResetCounter(IMonitor monitor, ArgumentParser args) + { + if (args.TryGet(1, "type", out string type, false)) + { + args.TryGet(2, "name", out string name); + + switch (type) + { + case "category": + SCore.PerformanceCounterManager.ResetCategory(name); + monitor.Log($"All performance counters for category {name} are now cleared.", LogLevel.Info); + break; + case "mod": + SCore.PerformanceCounterManager.ResetSource(name); + monitor.Log($"All performance counters for mod {name} are now cleared.", LogLevel.Info); + break; + } + } + else + { + SCore.PerformanceCounterManager.Reset(); + monitor.Log("All performance counters are now cleared.", LogLevel.Info); + } + } + + private void DisplayPerformanceCounterSummary(IMonitor monitor, ArgumentParser args) + { + IEnumerable data; + + if (!args.TryGet(1, "mode", out string mode, false)) + { + mode = "important"; + } + + switch (mode) + { + case null: + case "important": + data = SCore.PerformanceCounterManager.PerformanceCounterCollections.Where(p => p.IsImportant); + break; + case "all": + data = SCore.PerformanceCounterManager.PerformanceCounterCollections; + break; + default: + data = SCore.PerformanceCounterManager.PerformanceCounterCollections.Where(p => + p.Name.ToLowerInvariant().Contains(mode.ToLowerInvariant())); + break; + } + + StringBuilder sb = new StringBuilder(); + + sb.AppendLine("Summary:"); + sb.AppendLine(this.GetTableString( + data: data, + header: new[] {"Collection", "Avg Calls/s", "Avg Execution Time (Game)", "Avg Execution Time (Mods)", "Avg Execution Time (Game+Mods)"}, + getRow: item => new[] + { + item.Name, + item.GetAverageCallsPerSecond().ToString(), + this.FormatMilliseconds(item.GetGameAverageExecutionTime()), + this.FormatMilliseconds(item.GetModsAverageExecutionTime()), + this.FormatMilliseconds(item.GetAverageExecutionTime()) + } + )); + + monitor.Log(sb.ToString(), LogLevel.Info); + } + + private void DisplayPerformanceCounterDetail(IMonitor monitor, ArgumentParser args) + { + List collections = new List(); + TimeSpan averageInterval = TimeSpan.FromSeconds(60); + double? thresholdMilliseconds = null; + string sourceFilter = null; + + if (args.TryGet(1, "collection", out string collectionName)) + { + collections.AddRange(SCore.PerformanceCounterManager.PerformanceCounterCollections.Where(collection => collection.Name.ToLowerInvariant().Contains(collectionName.ToLowerInvariant()))); + + if (args.IsDecimal(2) && args.TryGetDecimal(2, "threshold", out decimal value, false)) + { + thresholdMilliseconds = (double?) value; + } + else + { + if (args.TryGet(2, "source", out string sourceName, false)) + { + sourceFilter = sourceName; + } + } + } + + foreach (var c in collections) + { + this.DisplayPerformanceCollectionDetail(monitor, c, averageInterval, thresholdMilliseconds, sourceFilter); + } + } + + private void DisplayPerformanceCollectionDetail(IMonitor monitor, PerformanceCounterCollection collection, + TimeSpan averageInterval, double? thresholdMilliseconds, string sourceFilter = null) + { + StringBuilder sb = new StringBuilder($"Performance Counter for {collection.Name}:\n\n"); + + IEnumerable> data = collection.PerformanceCounters; + + if (sourceFilter != null) + { + data = collection.PerformanceCounters.Where(p => + p.Value.Source.ToLowerInvariant().Contains(sourceFilter.ToLowerInvariant())); + } + + if (thresholdMilliseconds != null) + { + data = data.Where(p => p.Value.GetAverage(averageInterval) >= thresholdMilliseconds); + } + + sb.AppendLine(this.GetTableString( + data: data, + header: new[] {"Mod", $"Avg Execution Time (last {(int) averageInterval.TotalSeconds}s)", "Last Execution Time", "Peak Execution Time"}, + getRow: item => new[] + { + item.Key, + this.FormatMilliseconds(item.Value.GetAverage(averageInterval), thresholdMilliseconds), + this.FormatMilliseconds(item.Value.GetLastEntry()?.Elapsed.TotalMilliseconds), + this.FormatMilliseconds(item.Value.GetPeak()?.Elapsed.TotalMilliseconds) + } + )); + + monitor.Log(sb.ToString(), LogLevel.Info); + } + + private string FormatMilliseconds(double? milliseconds, double? thresholdMilliseconds = null) + { + if (milliseconds == null || (thresholdMilliseconds != null && milliseconds < thresholdMilliseconds)) + { + return "-"; + } + + return ((double) milliseconds).ToString("F2"); + } + + /// Get the command description. + private static string GetDescription() + { + StringBuilder sb = new StringBuilder(); + + sb.AppendLine("Displays and configured performance counters."); + sb.AppendLine(); + sb.AppendLine("A performance counter records the invocation time of in-game events being"); + sb.AppendLine("processed by mods or the game itself. See 'concepts' for a detailed explanation."); + sb.AppendLine(); + sb.AppendLine("Usage: pc "); + sb.AppendLine(); + sb.AppendLine("Commands:"); + sb.AppendLine(); + sb.AppendLine(" summary|sum|s Displays a summary of important or all collections"); + sb.AppendLine(" detail|d Shows performance counter information for a given collection"); + sb.AppendLine(" reset|r Resets the performance counters"); + sb.AppendLine(" monitor Configures monitoring settings"); + sb.AppendLine(" examples Displays various examples"); + sb.AppendLine(" concepts Displays an explanation of the performance counter concepts"); + sb.AppendLine(" help Displays verbose help for the available commands"); + sb.AppendLine(); + sb.AppendLine("To get help for a specific command, use 'pc help ', for example:"); + sb.AppendLine("pc help summary"); + sb.AppendLine(); + sb.AppendLine("Defaults to summary if no command is given."); + sb.AppendLine(); + return sb.ToString(); } } } diff --git a/src/SMAPI.Mods.ConsoleCommands/SMAPI.Mods.ConsoleCommands.csproj b/src/SMAPI.Mods.ConsoleCommands/SMAPI.Mods.ConsoleCommands.csproj index ce35bf73..f073ac21 100644 --- a/src/SMAPI.Mods.ConsoleCommands/SMAPI.Mods.ConsoleCommands.csproj +++ b/src/SMAPI.Mods.ConsoleCommands/SMAPI.Mods.ConsoleCommands.csproj @@ -67,6 +67,10 @@ + + + + diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index 0923494c..76cb6f89 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -56,7 +56,7 @@ namespace StardewModdingAPI internal const string HomePageUrl = "https://smapi.io"; /// The URL of the SMAPI home page. - internal const string GamePerformanceCounterName = "-internal-"; + internal const string GamePerformanceCounterName = ""; /// The absolute path to the folder containing SMAPI's internal files. internal static readonly string InternalFilesPath = Program.DllSearchPath; diff --git a/src/SMAPI/Framework/Events/EventManager.cs b/src/SMAPI/Framework/Events/EventManager.cs index 892cbc7b..9c65a6cc 100644 --- a/src/SMAPI/Framework/Events/EventManager.cs +++ b/src/SMAPI/Framework/Events/EventManager.cs @@ -1,5 +1,6 @@ using System.Diagnostics.CodeAnalysis; using StardewModdingAPI.Events; +using StardewModdingAPI.Framework.PerformanceCounter; namespace StardewModdingAPI.Framework.Events { @@ -173,10 +174,10 @@ namespace StardewModdingAPI.Framework.Events /// Construct an instance. /// Writes messages to the log. /// The mod registry with which to identify mods. - public EventManager(IMonitor monitor, ModRegistry modRegistry) + public EventManager(IMonitor monitor, ModRegistry modRegistry, PerformanceCounterManager performanceCounterManager) { // create shortcut initializers - ManagedEvent ManageEventOf(string typeName, string eventName) => new ManagedEvent($"{typeName}.{eventName}", monitor, modRegistry); + ManagedEvent ManageEventOf(string typeName, string eventName) => new ManagedEvent($"{typeName}.{eventName}", monitor, modRegistry, performanceCounterManager); // init events (new) this.MenuChanged = ManageEventOf(nameof(IModEvents.Display), nameof(IDisplayEvents.MenuChanged)); diff --git a/src/SMAPI/Framework/Events/IManagedEvent.cs b/src/SMAPI/Framework/Events/IManagedEvent.cs new file mode 100644 index 00000000..04476866 --- /dev/null +++ b/src/SMAPI/Framework/Events/IManagedEvent.cs @@ -0,0 +1,7 @@ +namespace StardewModdingAPI.Framework.Events +{ + internal interface IManagedEvent + { + string GetName(); + } +} diff --git a/src/SMAPI/Framework/Events/ManagedEvent.cs b/src/SMAPI/Framework/Events/ManagedEvent.cs index bb915738..bba94c35 100644 --- a/src/SMAPI/Framework/Events/ManagedEvent.cs +++ b/src/SMAPI/Framework/Events/ManagedEvent.cs @@ -1,15 +1,13 @@ using System; using System.Collections.Generic; -using System.Diagnostics; using System.Linq; -using StardewModdingAPI.Framework.Utilities; -using PerformanceCounter = StardewModdingAPI.Framework.PerformanceCounter.PerformanceCounter; +using PerformanceCounterManager = StardewModdingAPI.Framework.PerformanceCounter.PerformanceCounterManager; namespace StardewModdingAPI.Framework.Events { /// An event wrapper which intercepts and logs errors in handler code. /// The event arguments type. - internal class ManagedEvent: IPerformanceCounterEvent + internal class ManagedEvent: IManagedEvent { /********* ** Fields @@ -32,38 +30,8 @@ namespace StardewModdingAPI.Framework.Events /// The cached invocation list. private EventHandler[] CachedInvocationList; - public IDictionary PerformanceCounters { get; } = new Dictionary(); - - private readonly Stopwatch Stopwatch = new Stopwatch(); - - private long EventCallCount = 0; - - private readonly DateTime StartDateTime = DateTime.Now; - - public string GetEventName() - { - return this.EventName; - } - - public double GetGameAverageExecutionTime() - { - if (this.PerformanceCounters.TryGetValue(Constants.GamePerformanceCounterName, out PerformanceCounter.PerformanceCounter gameExecTime)) - { - return gameExecTime.GetAverage(); - } - - return 0; - } - - public double GetModsAverageExecutionTime() - { - return this.PerformanceCounters.Where(p => p.Key != Constants.GamePerformanceCounterName).Sum(p => p.Value.GetAverage()); - } - - public double GetAverageExecutionTime() - { - return this.PerformanceCounters.Sum(p => p.Value.GetAverage()); - } + /// The performance counter manager. + private readonly PerformanceCounterManager PerformanceCounterManager; /********* ** Public methods @@ -72,11 +40,18 @@ namespace StardewModdingAPI.Framework.Events /// A human-readable name for the event. /// Writes messages to the log. /// The mod registry with which to identify mods. - public ManagedEvent(string eventName, IMonitor monitor, ModRegistry modRegistry) + public ManagedEvent(string eventName, IMonitor monitor, ModRegistry modRegistry, PerformanceCounterManager performanceCounterManager) { this.EventName = eventName; this.Monitor = monitor; this.ModRegistry = modRegistry; + this.PerformanceCounterManager = performanceCounterManager; + } + + /// Gets the event name. + public string GetName() + { + return this.EventName; } /// Get whether anything is listening to the event. @@ -99,8 +74,6 @@ namespace StardewModdingAPI.Framework.Events { this.Event += handler; this.AddTracking(mod, handler, this.Event?.GetInvocationList().Cast>()); - - } /// Remove an event handler. @@ -111,18 +84,6 @@ namespace StardewModdingAPI.Framework.Events this.RemoveTracking(handler, this.Event?.GetInvocationList().Cast>()); } - public long GetAverageCallsPerSecond() - { - long runtimeInSeconds = (long)DateTime.Now.Subtract(this.StartDateTime).TotalSeconds; - - if (runtimeInSeconds == 0) - { - return 0; - } - - return this.EventCallCount / runtimeInSeconds; - } - /// Raise the event and notify all handlers. /// The event arguments to pass. public void Raise(TEventArgs args) @@ -130,37 +91,23 @@ namespace StardewModdingAPI.Framework.Events if (this.Event == null) return; - this.EventCallCount++; + + this.PerformanceCounterManager.BeginTrackInvocation(this.EventName); foreach (EventHandler handler in this.CachedInvocationList) { try { - var performanceCounterEntry = new PerformanceCounterEntry() - { - EventTime = DateTime.Now - }; - - this.Stopwatch.Reset(); - this.Stopwatch.Start(); - handler.Invoke(null, args); - this.Stopwatch.Stop(); - performanceCounterEntry.Elapsed = this.Stopwatch.Elapsed; - - string modName = this.GetSourceMod(handler)?.DisplayName ?? Constants.GamePerformanceCounterName; - - if (!this.PerformanceCounters.ContainsKey(modName)) - { - this.PerformanceCounters.Add(modName, new PerformanceCounter.PerformanceCounter($"{modName}.{this.EventName}")); - } - this.PerformanceCounters[modName].Add(performanceCounterEntry); - + this.PerformanceCounterManager.Track(this.EventName, this.GetModNameForPerformanceCounters(handler), + () => handler.Invoke(null, args)); } catch (Exception ex) { this.LogError(handler, ex); } } + + this.PerformanceCounterManager.EndTrackInvocation(this.EventName); } /// Raise the event and notify all handlers. @@ -191,6 +138,20 @@ namespace StardewModdingAPI.Framework.Events /********* ** Private methods *********/ + + private string GetModNameForPerformanceCounters(EventHandler handler) + { + IModMetadata mod = this.GetSourceMod(handler); + + if (mod == null) + { + return Constants.GamePerformanceCounterName; + } + + return mod.HasManifest() ? mod.Manifest.UniqueID : mod.DisplayName; + + } + /// Track an event handler. /// The mod which added the handler. /// The event handler. diff --git a/src/SMAPI/Framework/PerformanceCounter/AlertContext.cs b/src/SMAPI/Framework/PerformanceCounter/AlertContext.cs new file mode 100644 index 00000000..c4a57a49 --- /dev/null +++ b/src/SMAPI/Framework/PerformanceCounter/AlertContext.cs @@ -0,0 +1,14 @@ +namespace StardewModdingAPI.Framework.PerformanceCounter +{ + public struct AlertContext + { + public string Source; + public double Elapsed; + + public AlertContext(string source, double elapsed) + { + this.Source = source; + this.Elapsed = elapsed; + } + } +} diff --git a/src/SMAPI/Framework/PerformanceCounter/AlertEntry.cs b/src/SMAPI/Framework/PerformanceCounter/AlertEntry.cs new file mode 100644 index 00000000..284af1ce --- /dev/null +++ b/src/SMAPI/Framework/PerformanceCounter/AlertEntry.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; + +namespace StardewModdingAPI.Framework.PerformanceCounter +{ + internal struct AlertEntry + { + public PerformanceCounterCollection Collection; + public double ExecutionTimeMilliseconds; + public double Threshold; + public List Context; + + public AlertEntry(PerformanceCounterCollection collection, double executionTimeMilliseconds, double threshold, List context) + { + this.Collection = collection; + this.ExecutionTimeMilliseconds = executionTimeMilliseconds; + this.Threshold = threshold; + this.Context = context; + } + } +} diff --git a/src/SMAPI/Framework/PerformanceCounter/EventPerformanceCounterCategory.cs b/src/SMAPI/Framework/PerformanceCounter/EventPerformanceCounterCategory.cs deleted file mode 100644 index 14f74317..00000000 --- a/src/SMAPI/Framework/PerformanceCounter/EventPerformanceCounterCategory.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace StardewModdingAPI.Framework.Utilities -{ - public class EventPerformanceCounterCategory - { - public IPerformanceCounterEvent Event { get; } - public double MonitorThreshold { get; } - public bool IsImportant { get; } - public bool Monitor { get; } - - public EventPerformanceCounterCategory(IPerformanceCounterEvent @event, bool isImportant) - { - this.Event = @event; - this.IsImportant = isImportant; - } - } -} diff --git a/src/SMAPI/Framework/PerformanceCounter/EventPerformanceCounterCollection.cs b/src/SMAPI/Framework/PerformanceCounter/EventPerformanceCounterCollection.cs new file mode 100644 index 00000000..1aec28f3 --- /dev/null +++ b/src/SMAPI/Framework/PerformanceCounter/EventPerformanceCounterCollection.cs @@ -0,0 +1,11 @@ +using StardewModdingAPI.Framework.Events; + +namespace StardewModdingAPI.Framework.PerformanceCounter +{ + internal class EventPerformanceCounterCollection: PerformanceCounterCollection + { + public EventPerformanceCounterCollection(PerformanceCounterManager manager, IManagedEvent @event, bool isImportant) : base(manager, @event.GetName(), isImportant) + { + } + } +} diff --git a/src/SMAPI/Framework/PerformanceCounter/IPerformanceCounterEvent.cs b/src/SMAPI/Framework/PerformanceCounter/IPerformanceCounterEvent.cs index 6b83586d..1bcf4fa0 100644 --- a/src/SMAPI/Framework/PerformanceCounter/IPerformanceCounterEvent.cs +++ b/src/SMAPI/Framework/PerformanceCounter/IPerformanceCounterEvent.cs @@ -7,7 +7,6 @@ namespace StardewModdingAPI.Framework.Utilities { string GetEventName(); long GetAverageCallsPerSecond(); - IDictionary PerformanceCounters { get; } double GetGameAverageExecutionTime(); double GetModsAverageExecutionTime(); diff --git a/src/SMAPI/Framework/PerformanceCounter/PerformanceCounter.cs b/src/SMAPI/Framework/PerformanceCounter/PerformanceCounter.cs index 0b0275b7..3dbc693a 100644 --- a/src/SMAPI/Framework/PerformanceCounter/PerformanceCounter.cs +++ b/src/SMAPI/Framework/PerformanceCounter/PerformanceCounter.cs @@ -6,22 +6,25 @@ using StardewModdingAPI.Framework.Utilities; namespace StardewModdingAPI.Framework.PerformanceCounter { - public class PerformanceCounter + internal class PerformanceCounter { private const int MAX_ENTRIES = 16384; - public string Name { get; } + public string Source { get; } public static Stopwatch Stopwatch = new Stopwatch(); public static long TotalNumEventsLogged; - + public double MonitorThresholdMilliseconds { get; set; } + public bool Monitor { get; set; } + private readonly PerformanceCounterCollection ParentCollection; private readonly CircularBuffer _counter; private PerformanceCounterEntry? PeakPerformanceCounterEntry; - public PerformanceCounter(string name) + public PerformanceCounter(PerformanceCounterCollection parentCollection, string source) { - this.Name = name; + this.ParentCollection = parentCollection; + this.Source = source; this._counter = new CircularBuffer(PerformanceCounter.MAX_ENTRIES); } @@ -47,6 +50,11 @@ namespace StardewModdingAPI.Framework.PerformanceCounter PerformanceCounter.Stopwatch.Start(); this._counter.Put(entry); + if (this.Monitor && entry.Elapsed.TotalMilliseconds > this.MonitorThresholdMilliseconds) + { + this.ParentCollection.AddAlert(entry.Elapsed.TotalMilliseconds, this.MonitorThresholdMilliseconds, new AlertContext(this.Source, entry.Elapsed.TotalMilliseconds)); + } + if (this.PeakPerformanceCounterEntry == null) { this.PeakPerformanceCounterEntry = entry; diff --git a/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterCollection.cs b/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterCollection.cs new file mode 100644 index 00000000..343fddf6 --- /dev/null +++ b/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterCollection.cs @@ -0,0 +1,144 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using StardewModdingAPI.Framework.Utilities; + +namespace StardewModdingAPI.Framework.PerformanceCounter +{ + internal class PerformanceCounterCollection + { + public IDictionary PerformanceCounters { get; } = new Dictionary(); + private DateTime StartDateTime = DateTime.Now; + private long CallCount; + public string Name { get; private set; } + public bool IsImportant { get; set; } + private readonly Stopwatch Stopwatch = new Stopwatch(); + private readonly PerformanceCounterManager PerformanceCounterManager; + public double MonitorThresholdMilliseconds { get; set; } + public bool Monitor { get; set; } + private readonly List TriggeredPerformanceCounters = new List(); + + public PerformanceCounterCollection(PerformanceCounterManager performanceCounterManager, string name, bool isImportant) + { + this.Name = name; + this.PerformanceCounterManager = performanceCounterManager; + this.IsImportant = isImportant; + } + + public PerformanceCounterCollection(PerformanceCounterManager performanceCounterManager, string name) + { + this.PerformanceCounterManager = performanceCounterManager; + this.Name = name; + } + + public void Track(string source, PerformanceCounterEntry entry) + { + if (!this.PerformanceCounters.ContainsKey(source)) + { + this.PerformanceCounters.Add(source, new PerformanceCounter(this, source)); + } + this.PerformanceCounters[source].Add(entry); + + if (this.Monitor) + { + this.TriggeredPerformanceCounters.Add(new AlertContext(source, entry.Elapsed.TotalMilliseconds)); + } + } + + public double GetModsAverageExecutionTime() + { + return this.PerformanceCounters.Where(p => p.Key != Constants.GamePerformanceCounterName).Sum(p => p.Value.GetAverage()); + } + + public double GetAverageExecutionTime() + { + return this.PerformanceCounters.Sum(p => p.Value.GetAverage()); + } + + public double GetGameAverageExecutionTime() + { + if (this.PerformanceCounters.TryGetValue(Constants.GamePerformanceCounterName, out PerformanceCounter gameExecTime)) + { + return gameExecTime.GetAverage(); + } + + return 0; + } + + public void BeginTrackInvocation() + { + if (this.Monitor) + { + this.TriggeredPerformanceCounters.Clear(); + this.Stopwatch.Reset(); + this.Stopwatch.Start(); + } + + this.CallCount++; + + } + + public void EndTrackInvocation() + { + if (!this.Monitor) return; + + this.Stopwatch.Stop(); + if (this.Stopwatch.Elapsed.TotalMilliseconds >= this.MonitorThresholdMilliseconds) + { + this.AddAlert(this.Stopwatch.Elapsed.TotalMilliseconds, + this.MonitorThresholdMilliseconds, this.TriggeredPerformanceCounters); + } + } + + public void AddAlert(double executionTimeMilliseconds, double threshold, List alerts) + { + this.PerformanceCounterManager.AddAlert(new AlertEntry(this, executionTimeMilliseconds, + threshold, alerts)); + } + + public void AddAlert(double executionTimeMilliseconds, double threshold, AlertContext alert) + { + this.AddAlert(executionTimeMilliseconds, threshold, new List() {alert}); + } + + public void ResetCallsPerSecond() + { + this.CallCount = 0; + this.StartDateTime = DateTime.Now; + } + + public void Reset() + { + foreach (var i in this.PerformanceCounters) + { + i.Value.Reset(); + i.Value.ResetPeak(); + } + } + + public void ResetSource(string source) + { + foreach (var i in this.PerformanceCounters) + { + if (i.Value.Source.Equals(source, StringComparison.InvariantCultureIgnoreCase)) + { + i.Value.Reset(); + i.Value.ResetPeak(); + } + } + } + + public long GetAverageCallsPerSecond() + { + long runtimeInSeconds = (long) DateTime.Now.Subtract(this.StartDateTime).TotalSeconds; + + if (runtimeInSeconds == 0) + { + return 0; + } + + return this.CallCount / runtimeInSeconds; + } + } +} diff --git a/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterManager.cs b/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterManager.cs index ae7258e2..9e77e2fa 100644 --- a/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterManager.cs +++ b/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterManager.cs @@ -1,4 +1,8 @@ +using System; using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; using StardewModdingAPI.Framework.Events; using StardewModdingAPI.Framework.Utilities; @@ -6,92 +10,187 @@ namespace StardewModdingAPI.Framework.PerformanceCounter { internal class PerformanceCounterManager { - public HashSet PerformanceCounterEvents = new HashSet(); + public HashSet PerformanceCounterCollections = new HashSet(); + public List Alerts = new List(); + private readonly IMonitor Monitor; + private readonly Stopwatch Stopwatch = new Stopwatch(); - private readonly EventManager EventManager; - - public PerformanceCounterManager(EventManager eventManager) + public PerformanceCounterManager(IMonitor monitor) { - this.EventManager = eventManager; - this.InitializePerformanceCounterEvents(); + this.Monitor = monitor; } public void Reset() { - foreach (var performanceCounter in this.PerformanceCounterEvents) + foreach (var performanceCounter in this.PerformanceCounterCollections) { - this.ResetCategory(performanceCounter); + foreach (var eventPerformanceCounter in performanceCounter.PerformanceCounters) + { + eventPerformanceCounter.Value.Reset(); + } } } - public void ResetCategory(EventPerformanceCounterCategory category) + /// Print any queued messages. + public void PrintQueued() { - foreach (var eventPerformanceCounter in category.Event.PerformanceCounters) + if (this.Alerts.Count == 0) + { + return; + } + StringBuilder sb = new StringBuilder(); + + foreach (var alert in this.Alerts) { - eventPerformanceCounter.Value.Reset(); + sb.AppendLine($"{alert.Collection.Name} took {alert.ExecutionTimeMilliseconds:F2}ms (exceeded threshold of {alert.Threshold:F2}ms)"); + + foreach (var context in alert.Context) + { + sb.AppendLine($"{context.Source}: {context.Elapsed:F2}ms"); + } } + + this.Alerts.Clear(); + + this.Monitor.Log(sb.ToString(), LogLevel.Error); + } + + public void BeginTrackInvocation(string collectionName) + { + this.GetOrCreateCollectionByName(collectionName).BeginTrackInvocation(); + } + + public void EndTrackInvocation(string collectionName) + { + this.GetOrCreateCollectionByName(collectionName).EndTrackInvocation(); } - private void InitializePerformanceCounterEvents() + public void Track(string collectionName, string modName, Action action) { - this.PerformanceCounterEvents = new HashSet() + DateTime eventTime = DateTime.UtcNow; + this.Stopwatch.Reset(); + this.Stopwatch.Start(); + + try + { + action(); + } + finally { - new EventPerformanceCounterCategory(this.EventManager.MenuChanged, false), + this.Stopwatch.Stop(); - // Rendering Events - new EventPerformanceCounterCategory(this.EventManager.Rendering, true), - new EventPerformanceCounterCategory(this.EventManager.Rendered, true), - new EventPerformanceCounterCategory(this.EventManager.RenderingWorld, true), - new EventPerformanceCounterCategory(this.EventManager.RenderedWorld, true), - new EventPerformanceCounterCategory(this.EventManager.RenderingActiveMenu, true), - new EventPerformanceCounterCategory(this.EventManager.RenderedActiveMenu, true), - new EventPerformanceCounterCategory(this.EventManager.RenderingHud, true), - new EventPerformanceCounterCategory(this.EventManager.RenderedHud, true), - - new EventPerformanceCounterCategory(this.EventManager.WindowResized, false), - new EventPerformanceCounterCategory(this.EventManager.GameLaunched, false), - new EventPerformanceCounterCategory(this.EventManager.UpdateTicking, true), - new EventPerformanceCounterCategory(this.EventManager.UpdateTicked, true), - new EventPerformanceCounterCategory(this.EventManager.OneSecondUpdateTicking, true), - new EventPerformanceCounterCategory(this.EventManager.OneSecondUpdateTicked, true), - - new EventPerformanceCounterCategory(this.EventManager.SaveCreating, false), - new EventPerformanceCounterCategory(this.EventManager.SaveCreated, false), - new EventPerformanceCounterCategory(this.EventManager.Saving, false), - new EventPerformanceCounterCategory(this.EventManager.Saved, false), - - new EventPerformanceCounterCategory(this.EventManager.DayStarted, false), - new EventPerformanceCounterCategory(this.EventManager.DayEnding, false), - - new EventPerformanceCounterCategory(this.EventManager.TimeChanged, true), - - new EventPerformanceCounterCategory(this.EventManager.ReturnedToTitle, false), - - new EventPerformanceCounterCategory(this.EventManager.ButtonPressed, true), - new EventPerformanceCounterCategory(this.EventManager.ButtonReleased, true), - new EventPerformanceCounterCategory(this.EventManager.CursorMoved, true), - new EventPerformanceCounterCategory(this.EventManager.MouseWheelScrolled, true), - - new EventPerformanceCounterCategory(this.EventManager.PeerContextReceived, true), - new EventPerformanceCounterCategory(this.EventManager.ModMessageReceived, true), - new EventPerformanceCounterCategory(this.EventManager.PeerDisconnected, true), - new EventPerformanceCounterCategory(this.EventManager.InventoryChanged, true), - new EventPerformanceCounterCategory(this.EventManager.LevelChanged, true), - new EventPerformanceCounterCategory(this.EventManager.Warped, true), - - new EventPerformanceCounterCategory(this.EventManager.LocationListChanged, true), - new EventPerformanceCounterCategory(this.EventManager.BuildingListChanged, true), - new EventPerformanceCounterCategory(this.EventManager.LocationListChanged, true), - new EventPerformanceCounterCategory(this.EventManager.DebrisListChanged, true), - new EventPerformanceCounterCategory(this.EventManager.LargeTerrainFeatureListChanged, true), - new EventPerformanceCounterCategory(this.EventManager.NpcListChanged, true), - new EventPerformanceCounterCategory(this.EventManager.ObjectListChanged, true), - new EventPerformanceCounterCategory(this.EventManager.ChestInventoryChanged, true), - new EventPerformanceCounterCategory(this.EventManager.TerrainFeatureListChanged, true), - new EventPerformanceCounterCategory(this.EventManager.LoadStageChanged, false), - new EventPerformanceCounterCategory(this.EventManager.UnvalidatedUpdateTicking, true), - new EventPerformanceCounterCategory(this.EventManager.UnvalidatedUpdateTicked, true), + this.GetOrCreateCollectionByName(collectionName).Track(modName, new PerformanceCounterEntry + { + EventTime = eventTime, + Elapsed = this.Stopwatch.Elapsed + }); + } + } + + public PerformanceCounterCollection GetCollectionByName(string name) + { + return this.PerformanceCounterCollections.FirstOrDefault(collection => collection.Name == name); + } + + public PerformanceCounterCollection GetOrCreateCollectionByName(string name) + { + PerformanceCounterCollection collection = this.GetCollectionByName(name); + if (collection == null) + { + collection = new PerformanceCounterCollection(this, name); + this.PerformanceCounterCollections.Add(collection); + } + + return collection; + } + + public void ResetCategory(string name) + { + foreach (var performanceCounterCollection in this.PerformanceCounterCollections) + { + if (performanceCounterCollection.Name.Equals(name, StringComparison.InvariantCultureIgnoreCase)) + { + performanceCounterCollection.ResetCallsPerSecond(); + performanceCounterCollection.Reset(); + } + } + } + + public void ResetSource(string name) + { + foreach (var performanceCounterCollection in this.PerformanceCounterCollections) + { + performanceCounterCollection.ResetSource(name); + } + } + + + public void AddAlert(AlertEntry entry) + { + this.Alerts.Add(entry); + } + + public void InitializePerformanceCounterEvents(EventManager eventManager) + { + this.PerformanceCounterCollections = new HashSet() + { + new EventPerformanceCounterCollection(this, eventManager.MenuChanged, false), + + + // Rendering Events + new EventPerformanceCounterCollection(this, eventManager.Rendering, true), + new EventPerformanceCounterCollection(this, eventManager.Rendered, true), + new EventPerformanceCounterCollection(this, eventManager.RenderingWorld, true), + new EventPerformanceCounterCollection(this, eventManager.RenderedWorld, true), + new EventPerformanceCounterCollection(this, eventManager.RenderingActiveMenu, true), + new EventPerformanceCounterCollection(this, eventManager.RenderedActiveMenu, true), + new EventPerformanceCounterCollection(this, eventManager.RenderingHud, true), + new EventPerformanceCounterCollection(this, eventManager.RenderedHud, true), + + new EventPerformanceCounterCollection(this, eventManager.WindowResized, false), + new EventPerformanceCounterCollection(this, eventManager.GameLaunched, false), + new EventPerformanceCounterCollection(this, eventManager.UpdateTicking, true), + new EventPerformanceCounterCollection(this, eventManager.UpdateTicked, true), + new EventPerformanceCounterCollection(this, eventManager.OneSecondUpdateTicking, true), + new EventPerformanceCounterCollection(this, eventManager.OneSecondUpdateTicked, true), + + new EventPerformanceCounterCollection(this, eventManager.SaveCreating, false), + new EventPerformanceCounterCollection(this, eventManager.SaveCreated, false), + new EventPerformanceCounterCollection(this, eventManager.Saving, false), + new EventPerformanceCounterCollection(this, eventManager.Saved, false), + + new EventPerformanceCounterCollection(this, eventManager.DayStarted, false), + new EventPerformanceCounterCollection(this, eventManager.DayEnding, false), + + new EventPerformanceCounterCollection(this, eventManager.TimeChanged, true), + + new EventPerformanceCounterCollection(this, eventManager.ReturnedToTitle, false), + + new EventPerformanceCounterCollection(this, eventManager.ButtonPressed, true), + new EventPerformanceCounterCollection(this, eventManager.ButtonReleased, true), + new EventPerformanceCounterCollection(this, eventManager.CursorMoved, true), + new EventPerformanceCounterCollection(this, eventManager.MouseWheelScrolled, true), + + new EventPerformanceCounterCollection(this, eventManager.PeerContextReceived, true), + new EventPerformanceCounterCollection(this, eventManager.ModMessageReceived, true), + new EventPerformanceCounterCollection(this, eventManager.PeerDisconnected, true), + new EventPerformanceCounterCollection(this, eventManager.InventoryChanged, true), + new EventPerformanceCounterCollection(this, eventManager.LevelChanged, true), + new EventPerformanceCounterCollection(this, eventManager.Warped, true), + + new EventPerformanceCounterCollection(this, eventManager.LocationListChanged, true), + new EventPerformanceCounterCollection(this, eventManager.BuildingListChanged, true), + new EventPerformanceCounterCollection(this, eventManager.LocationListChanged, true), + new EventPerformanceCounterCollection(this, eventManager.DebrisListChanged, true), + new EventPerformanceCounterCollection(this, eventManager.LargeTerrainFeatureListChanged, true), + new EventPerformanceCounterCollection(this, eventManager.NpcListChanged, true), + new EventPerformanceCounterCollection(this, eventManager.ObjectListChanged, true), + new EventPerformanceCounterCollection(this, eventManager.ChestInventoryChanged, true), + new EventPerformanceCounterCollection(this, eventManager.TerrainFeatureListChanged, true), + new EventPerformanceCounterCollection(this, eventManager.LoadStageChanged, false), + new EventPerformanceCounterCollection(this, eventManager.UnvalidatedUpdateTicking, false), + new EventPerformanceCounterCollection(this, eventManager.UnvalidatedUpdateTicked, false), }; } } diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index d1dba9ea..5b0c6691 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -25,7 +25,6 @@ using StardewModdingAPI.Framework.ModLoading; using StardewModdingAPI.Framework.Patching; using StardewModdingAPI.Framework.Reflection; using StardewModdingAPI.Framework.Serialization; -using StardewModdingAPI.Framework.Utilities; using StardewModdingAPI.Patches; using StardewModdingAPI.Toolkit; using StardewModdingAPI.Toolkit.Framework.Clients.WebApi; @@ -82,8 +81,6 @@ namespace StardewModdingAPI.Framework /// Manages SMAPI events for mods. private readonly EventManager EventManager; - private readonly PerformanceCounterManager PerformanceCounterManager; - /// Whether the game is currently running. private bool IsGameRunning; @@ -137,6 +134,10 @@ namespace StardewModdingAPI.Framework /// This is initialized after the game starts. This is accessed directly because it's not part of the normal class model. internal static DeprecationManager DeprecationManager { get; private set; } + /// Manages performance counters. + /// This is initialized after the game starts. This is accessed directly because it's not part of the normal class model. + internal static PerformanceCounterManager PerformanceCounterManager { get; private set; } + /********* ** Public methods @@ -165,8 +166,10 @@ namespace StardewModdingAPI.Framework ShowFullStampInConsole = this.Settings.DeveloperMode }; this.MonitorForGame = this.GetSecondaryMonitor("game"); - this.EventManager = new EventManager(this.Monitor, this.ModRegistry); - this.PerformanceCounterManager = new PerformanceCounterManager(this.EventManager); + + SCore.PerformanceCounterManager = new PerformanceCounterManager(this.Monitor); + this.EventManager = new EventManager(this.Monitor, this.ModRegistry, SCore.PerformanceCounterManager); + SCore.PerformanceCounterManager.InitializePerformanceCounterEvents(this.EventManager); SCore.DeprecationManager = new DeprecationManager(this.Monitor, this.ModRegistry); @@ -245,6 +248,7 @@ namespace StardewModdingAPI.Framework jsonHelper: this.Toolkit.JsonHelper, modRegistry: this.ModRegistry, deprecationManager: SCore.DeprecationManager, + performanceCounterManager: SCore.PerformanceCounterManager, onGameInitialized: this.InitializeAfterGameStart, onGameExiting: this.Dispose, cancellationToken: this.CancellationToken, @@ -488,20 +492,6 @@ namespace StardewModdingAPI.Framework this.Monitor.Log("Type 'help' for help, or 'help ' for a command's usage", LogLevel.Info); this.GameInstance.CommandManager.Add(null, "help", "Lists command documentation.\n\nUsage: help\nLists all available commands.\n\nUsage: help \n- cmd: The name of a command whose documentation to display.", this.HandleCommand); this.GameInstance.CommandManager.Add(null, "reload_i18n", "Reloads translation files for all mods.\n\nUsage: reload_i18n", this.HandleCommand); - this.GameInstance.CommandManager.Add(null, "performance_counters", - "Displays performance counters.\n\n"+ - "Usage: performance_counters\n" + - "Shows the most important event invocation times\n\n"+ - "Usage: performance_counters summary|sum [all|important|name]\n"+ - "- summary or sum: Forces summary mode\n"+ - "- all, important or name: Displays all event performance counters, only important ones, or a specific event by name (defaults to important)\n\n"+ - "Usage: performance_counters [name] [threshold]\n"+ - "Shows detailed performance counters for a specific event\n"+ - "- name: The (partial) name of the event\n"+ - "- threshold: The minimum avg execution time (ms) of the event\n\n"+ - "Usage: performance_counters reset\n"+ - "Resets all performance counters\n", this.HandleCommand); - this.GameInstance.CommandManager.Add(null, "pc", "Alias for performance_counters", this.HandleCommand); // start handling command line input Thread inputThread = new Thread(() => @@ -1317,176 +1307,11 @@ namespace StardewModdingAPI.Framework this.ReloadTranslations(this.ModRegistry.GetAll(contentPacks: false)); this.Monitor.Log("Reloaded translation files for all mods. This only affects new translations the mods fetch; if they cached some text, it may not be updated.", LogLevel.Info); break; - case "performance_counters": - case "pc": - this.DisplayPerformanceCounters(arguments.ToList()); - break; default: throw new NotSupportedException($"Unrecognized core SMAPI command '{name}'."); } } - /// Get an ASCII table to show tabular data in the console. - /// The data type. - /// The data to display. - /// The table header. - /// Returns a set of fields for a data value. - protected string GetTableString(IEnumerable data, string[] header, Func getRow) - { - // get table data - int[] widths = header.Select(p => p.Length).ToArray(); - string[][] rows = data - .Select(item => - { - string[] fields = getRow(item); - if (fields.Length != widths.Length) - throw new InvalidOperationException($"Expected {widths.Length} columns, but found {fields.Length}: {string.Join(", ", fields)}"); - - for (int i = 0; i < fields.Length; i++) - widths[i] = Math.Max(widths[i], fields[i].Length); - - return fields; - }) - .ToArray(); - - // render fields - List lines = new List(rows.Length + 2) - { - header, - header.Select((value, i) => "".PadRight(widths[i], '-')).ToArray() - }; - lines.AddRange(rows); - - return string.Join( - Environment.NewLine, - lines.Select(line => string.Join(" | ", line.Select((field, i) => field.PadLeft(widths[i], ' ')).ToArray()) - ) - ); - } - - private void DisplayPerformanceCounters(IList arguments) - { - bool showSummary = true; - bool showSummaryOnlyImportant = true; - string filterByName = null; - - if (arguments.Any()) - { - switch (arguments[0]) - { - case "summary": - case "sum": - showSummary = true; - - if (arguments.Count > 1) - { - switch (arguments[1].ToLower()) - { - case "all": - showSummaryOnlyImportant = false; - break; - case "important": - showSummaryOnlyImportant = true; - break; - default: - filterByName = arguments[1]; - break; - } - } - break; - case "reset": - this.PerformanceCounterManager.Reset(); - return; - default: - showSummary = false; - filterByName = arguments[0]; - break; - - } - } - var lastMinute = TimeSpan.FromSeconds(60); - - if (showSummary) - { - this.DisplayPerformanceCounterSummary(showSummaryOnlyImportant, filterByName); - } - else - { - var data = this.PerformanceCounterManager.PerformanceCounterEvents.Where(p => p.Event.GetEventName().ToLowerInvariant().Contains(filterByName.ToLowerInvariant())); - - foreach (var i in data) - { - this.DisplayPerformanceCounter(i, lastMinute); - } - } - - double avgTime = PerformanceCounter.PerformanceCounter.Stopwatch.ElapsedMilliseconds / (double)PerformanceCounter.PerformanceCounter.TotalNumEventsLogged; - this.Monitor.Log($"Logged {PerformanceCounter.PerformanceCounter.TotalNumEventsLogged} events in {PerformanceCounter.PerformanceCounter.Stopwatch.ElapsedMilliseconds}ms (avg {avgTime:F4}ms / event)"); - - } - - private void DisplayPerformanceCounterSummary(bool showOnlyImportant, string eventNameFilter = null) - { - StringBuilder sb = new StringBuilder($"Performance Counter Summary:\n\n"); - - IEnumerable data; - - if (eventNameFilter != null) - { - data = this.PerformanceCounterManager.PerformanceCounterEvents.Where(p => p.Event.GetEventName().ToLowerInvariant().Contains(eventNameFilter.ToLowerInvariant())); - } - else - { - if (showOnlyImportant) - { - data = this.PerformanceCounterManager.PerformanceCounterEvents.Where(p => p.IsImportant); - } - else - { - data = this.PerformanceCounterManager.PerformanceCounterEvents; - } - } - - - sb.AppendLine(this.GetTableString( - data: data, - header: new[] {"Event", "Avg Calls/s", "Avg Execution Time (Game)", "Avg Execution Time (Mods)", "Avg Execution Time (Game+Mods)"}, - getRow: item => new[] - { - item.Event.GetEventName(), - item.Event.GetAverageCallsPerSecond().ToString(), - item.Event.GetGameAverageExecutionTime().ToString("F2") + " ms", - item.Event.GetModsAverageExecutionTime().ToString("F2") + " ms", - item.Event.GetAverageExecutionTime().ToString("F2") + " ms" - } - )); - - this.Monitor.Log(sb.ToString(), LogLevel.Info); - } - - private void DisplayPerformanceCounter (EventPerformanceCounterCategory obj, TimeSpan averageInterval) - { - StringBuilder sb = new StringBuilder($"Performance Counter for {obj.Event.GetEventName()}:\n\n"); - - sb.AppendLine(this.GetTableString( - data: obj.Event.PerformanceCounters, - header: new[] {"Mod", $"Avg Execution Time over {(int)averageInterval.TotalSeconds}s", "Last Execution Time", "Peak Execution Time"}, - getRow: item => new[] - { - item.Key, - item.Value.GetAverage(averageInterval).ToString("F2") + " ms" ?? "-", - item.Value.GetLastEntry()?.Elapsed.TotalMilliseconds.ToString("F2") + " ms" ?? "-", - item.Value.GetPeak()?.Elapsed.TotalMilliseconds.ToString("F2") + " ms" ?? "-", - } - )); - - sb.AppendLine($"Average execution time (Game+Mods): {obj.Event.GetAverageExecutionTime():F2} ms"); - sb.AppendLine($"Average execution time (Game only) : {obj.Event.GetGameAverageExecutionTime():F2} ms"); - sb.AppendLine($"Average execution time (Mods only) : {obj.Event.GetModsAverageExecutionTime():F2} ms"); - - this.Monitor.Log(sb.ToString(), LogLevel.Info); - } - /// Redirect messages logged directly to the console to the given monitor. /// The monitor with which to log messages as the game. /// The message to log. diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index d6c3b836..8aba9b57 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -17,6 +17,7 @@ using StardewModdingAPI.Framework.Content; using StardewModdingAPI.Framework.Events; using StardewModdingAPI.Framework.Input; using StardewModdingAPI.Framework.Networking; +using StardewModdingAPI.Framework.PerformanceCounter; using StardewModdingAPI.Framework.Reflection; using StardewModdingAPI.Framework.StateTracking.Comparers; using StardewModdingAPI.Framework.StateTracking.Snapshots; @@ -58,6 +59,8 @@ namespace StardewModdingAPI.Framework /// Manages deprecation warnings. private readonly DeprecationManager DeprecationManager; + private readonly PerformanceCounterManager PerformanceCounterManager; + /// The maximum number of consecutive attempts SMAPI should make to recover from a draw error. private readonly Countdown DrawCrashTimer = new Countdown(60); // 60 ticks = roughly one second @@ -152,11 +155,12 @@ namespace StardewModdingAPI.Framework /// Encapsulates SMAPI's JSON file parsing. /// Tracks the installed mods. /// Manages deprecation warnings. + /// Manages performance monitoring. /// A callback to invoke after the game finishes initializing. /// A callback to invoke when the game exits. /// Propagates notification that SMAPI should exit. /// Whether to log network traffic. - internal SGame(Monitor monitor, IMonitor monitorForGame, Reflector reflection, Translator translator, EventManager eventManager, JsonHelper jsonHelper, ModRegistry modRegistry, DeprecationManager deprecationManager, Action onGameInitialized, Action onGameExiting, CancellationTokenSource cancellationToken, bool logNetworkTraffic) + internal SGame(Monitor monitor, IMonitor monitorForGame, Reflector reflection, Translator translator, EventManager eventManager, JsonHelper jsonHelper, ModRegistry modRegistry, DeprecationManager deprecationManager, PerformanceCounterManager performanceCounterManager, Action onGameInitialized, Action onGameExiting, CancellationTokenSource cancellationToken, bool logNetworkTraffic) { this.OnLoadingFirstAsset = SGame.ConstructorHack.OnLoadingFirstAsset; SGame.ConstructorHack = null; @@ -176,6 +180,7 @@ namespace StardewModdingAPI.Framework this.Reflection = reflection; this.Translator = translator; this.DeprecationManager = deprecationManager; + this.PerformanceCounterManager = performanceCounterManager; this.OnGameInitialized = onGameInitialized; this.OnGameExiting = onGameExiting; Game1.input = new SInputState(); @@ -307,6 +312,7 @@ namespace StardewModdingAPI.Framework try { this.DeprecationManager.PrintQueued(); + this.PerformanceCounterManager.PrintQueued(); /********* ** First-tick initialization @@ -382,7 +388,7 @@ namespace StardewModdingAPI.Framework // state while mods are running their code. This is risky, because data changes can // conflict (e.g. collection changed during enumeration errors) and data may change // unexpectedly from one mod instruction to the next. - // + // // Therefore we can just run Game1.Update here without raising any SMAPI events. There's // a small chance that the task will finish after we defer but before the game checks, // which means technically events should be raised, but the effects of missing one diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index b5e6307a..933590d3 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -11,7 +11,7 @@ using StardewModdingAPI.Framework; using StardewModdingAPI.Toolkit.Utilities; [assembly: InternalsVisibleTo("SMAPI.Tests")] -[assembly: InternalsVisibleTo("SMAPI.Mods.ConsoleCommands")] +[assembly: InternalsVisibleTo("ConsoleCommands")] [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] // Moq for unit testing namespace StardewModdingAPI { -- cgit From 219696275df054d25cd385f950eb01ee33312e76 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 11 Jan 2020 13:20:37 -0500 Subject: fix errors due to async threads creating content managers --- docs/release-notes.md | 1 + src/SMAPI/Framework/ContentCoordinator.cs | 113 ++++++++++++++++++------------ src/SMAPI/Framework/InternalExtensions.cs | 70 ++++++++++++++++++ 3 files changed, 140 insertions(+), 44 deletions(-) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index 5aa279b5..dd4158aa 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -6,6 +6,7 @@ * For players: * SMAPI now prevents mods from crashing the game with invalid schedule data. * Updated minimum game version (1.4 → 1.4.1). + * Fixed 'collection was modified' error when returning to title in rare cases. ## 3.1 Released 05 January 2019 for Stardew Valley 1.4 or later. diff --git a/src/SMAPI/Framework/ContentCoordinator.cs b/src/SMAPI/Framework/ContentCoordinator.cs index 82d3805b..0e62837c 100644 --- a/src/SMAPI/Framework/ContentCoordinator.cs +++ b/src/SMAPI/Framework/ContentCoordinator.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; +using System.Threading; using Microsoft.Xna.Framework.Content; using StardewModdingAPI.Framework.Content; using StardewModdingAPI.Framework.ContentManagers; @@ -48,6 +49,10 @@ namespace StardewModdingAPI.Framework /// Whether the content coordinator has been disposed. private bool IsDisposed; + /// A lock used to prevent asynchronous changes to the content manager list. + /// The game may adds content managers in asynchronous threads (e.g. when populating the load screen). + private readonly ReaderWriterLockSlim ContentManagerLock = new ReaderWriterLockSlim(); + /********* ** Accessors @@ -96,9 +101,12 @@ namespace StardewModdingAPI.Framework /// A name for the mod manager. Not guaranteed to be unique. public GameContentManager CreateGameContentManager(string name) { - GameContentManager manager = new GameContentManager(name, this.MainContentManager.ServiceProvider, this.MainContentManager.RootDirectory, this.MainContentManager.CurrentCulture, this, this.Monitor, this.Reflection, this.OnDisposing, this.OnLoadingFirstAsset); - this.ContentManagers.Add(manager); - return manager; + return this.ContentManagerLock.InWriteLock(() => + { + GameContentManager manager = new GameContentManager(name, this.MainContentManager.ServiceProvider, this.MainContentManager.RootDirectory, this.MainContentManager.CurrentCulture, this, this.Monitor, this.Reflection, this.OnDisposing, this.OnLoadingFirstAsset); + this.ContentManagers.Add(manager); + return manager; + }); } /// Get a new content manager which handles reading files from a SMAPI mod folder with support for unpacked files. @@ -107,20 +115,23 @@ namespace StardewModdingAPI.Framework /// The game content manager used for map tilesheets not provided by the mod. public ModContentManager CreateModContentManager(string name, string rootDirectory, IContentManager gameContentManager) { - ModContentManager manager = new ModContentManager( - name: name, - gameContentManager: gameContentManager, - serviceProvider: this.MainContentManager.ServiceProvider, - rootDirectory: rootDirectory, - currentCulture: this.MainContentManager.CurrentCulture, - coordinator: this, - monitor: this.Monitor, - reflection: this.Reflection, - jsonHelper: this.JsonHelper, - onDisposing: this.OnDisposing - ); - this.ContentManagers.Add(manager); - return manager; + return this.ContentManagerLock.InWriteLock(() => + { + ModContentManager manager = new ModContentManager( + name: name, + gameContentManager: gameContentManager, + serviceProvider: this.MainContentManager.ServiceProvider, + rootDirectory: rootDirectory, + currentCulture: this.MainContentManager.CurrentCulture, + coordinator: this, + monitor: this.Monitor, + reflection: this.Reflection, + jsonHelper: this.JsonHelper, + onDisposing: this.OnDisposing + ); + this.ContentManagers.Add(manager); + return manager; + }); } /// Get the current content locale. @@ -132,8 +143,11 @@ namespace StardewModdingAPI.Framework /// Perform any cleanup needed when the locale changes. public void OnLocaleChanged() { - foreach (IContentManager contentManager in this.ContentManagers) - contentManager.OnLocaleChanged(); + this.ContentManagerLock.InReadLock(() => + { + foreach (IContentManager contentManager in this.ContentManagers) + contentManager.OnLocaleChanged(); + }); } /// Get whether this asset is mapped to a mod folder. @@ -179,13 +193,16 @@ namespace StardewModdingAPI.Framework /// The internal SMAPI asset key. public T LoadManagedAsset(string contentManagerID, string relativePath) { - // get content manager - IContentManager contentManager = this.ContentManagers.FirstOrDefault(p => p.IsNamespaced && p.Name == contentManagerID); - if (contentManager == null) - throw new InvalidOperationException($"The '{contentManagerID}' prefix isn't handled by any mod."); - - // get fresh asset - return contentManager.Load(relativePath, this.DefaultLanguage, useCache: false); + return this.ContentManagerLock.InReadLock(() => + { + // get content manager + IContentManager contentManager = this.ContentManagers.FirstOrDefault(p => p.IsNamespaced && p.Name == contentManagerID); + if (contentManager == null) + throw new InvalidOperationException($"The '{contentManagerID}' prefix isn't handled by any mod."); + + // get fresh asset + return contentManager.Load(relativePath, this.DefaultLanguage, useCache: false); + }); } /// Purge matched assets from the cache. @@ -208,28 +225,31 @@ namespace StardewModdingAPI.Framework /// Returns the invalidated asset names. public IEnumerable InvalidateCache(Func predicate, bool dispose = false) { - // invalidate cache & track removed assets - IDictionary> removedAssets = new Dictionary>(StringComparer.InvariantCultureIgnoreCase); - foreach (IContentManager contentManager in this.ContentManagers) + return this.ContentManagerLock.InReadLock(() => { - foreach (var entry in contentManager.InvalidateCache(predicate, dispose)) + // invalidate cache & track removed assets + IDictionary> removedAssets = new Dictionary>(StringComparer.InvariantCultureIgnoreCase); + foreach (IContentManager contentManager in this.ContentManagers) { - if (!removedAssets.TryGetValue(entry.Key, out ISet assets)) - removedAssets[entry.Key] = assets = new HashSet(new ObjectReferenceComparer()); - assets.Add(entry.Value); + foreach (var entry in contentManager.InvalidateCache(predicate, dispose)) + { + if (!removedAssets.TryGetValue(entry.Key, out ISet assets)) + removedAssets[entry.Key] = assets = new HashSet(new ObjectReferenceComparer()); + assets.Add(entry.Value); + } } - } - // reload core game assets - if (removedAssets.Any()) - { - IDictionary propagated = this.CoreAssets.Propagate(this.MainContentManager, removedAssets.ToDictionary(p => p.Key, p => p.Value.First().GetType())); // use an intercepted content manager - this.Monitor.Log($"Invalidated {removedAssets.Count} asset names ({string.Join(", ", removedAssets.Keys.OrderBy(p => p, StringComparer.InvariantCultureIgnoreCase))}); propagated {propagated.Count(p => p.Value)} core assets.", LogLevel.Trace); - } - else - this.Monitor.Log("Invalidated 0 cache entries.", LogLevel.Trace); + // reload core game assets + if (removedAssets.Any()) + { + IDictionary propagated = this.CoreAssets.Propagate(this.MainContentManager, removedAssets.ToDictionary(p => p.Key, p => p.Value.First().GetType())); // use an intercepted content manager + this.Monitor.Log($"Invalidated {removedAssets.Count} asset names ({string.Join(", ", removedAssets.Keys.OrderBy(p => p, StringComparer.InvariantCultureIgnoreCase))}); propagated {propagated.Count(p => p.Value)} core assets.", LogLevel.Trace); + } + else + this.Monitor.Log("Invalidated 0 cache entries.", LogLevel.Trace); - return removedAssets.Keys; + return removedAssets.Keys; + }); } /// Dispose held resources. @@ -244,6 +264,8 @@ namespace StardewModdingAPI.Framework contentManager.Dispose(); this.ContentManagers.Clear(); this.MainContentManager = null; + + this.ContentManagerLock.Dispose(); } @@ -257,7 +279,10 @@ namespace StardewModdingAPI.Framework if (this.IsDisposed) return; - this.ContentManagers.Remove(contentManager); + this.ContentManagerLock.InWriteLock(() => + { + this.ContentManagers.Remove(contentManager); + }); } } } diff --git a/src/SMAPI/Framework/InternalExtensions.cs b/src/SMAPI/Framework/InternalExtensions.cs index c3155b1c..8b45e196 100644 --- a/src/SMAPI/Framework/InternalExtensions.cs +++ b/src/SMAPI/Framework/InternalExtensions.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Reflection; +using System.Threading; using Microsoft.Xna.Framework.Graphics; using StardewModdingAPI.Framework.Events; using StardewModdingAPI.Framework.Reflection; @@ -83,6 +84,75 @@ namespace StardewModdingAPI.Framework return exception; } + /**** + ** ReaderWriterLockSlim + ****/ + /// Run code within a read lock. + /// The lock to set. + /// The action to perform. + public static void InReadLock(this ReaderWriterLockSlim @lock, Action action) + { + @lock.EnterReadLock(); + try + { + action(); + } + finally + { + @lock.ExitReadLock(); + } + } + + /// Run code within a read lock. + /// The action's return value. + /// The lock to set. + /// The action to perform. + public static TReturn InReadLock(this ReaderWriterLockSlim @lock, Func action) + { + @lock.EnterReadLock(); + try + { + return action(); + } + finally + { + @lock.ExitReadLock(); + } + } + + /// Run code within a write lock. + /// The lock to set. + /// The action to perform. + public static void InWriteLock(this ReaderWriterLockSlim @lock, Action action) + { + @lock.EnterWriteLock(); + try + { + action(); + } + finally + { + @lock.ExitWriteLock(); + } + } + + /// Run code within a write lock. + /// The action's return value. + /// The lock to set. + /// The action to perform. + public static TReturn InWriteLock(this ReaderWriterLockSlim @lock, Func action) + { + @lock.EnterWriteLock(); + try + { + return action(); + } + finally + { + @lock.ExitWriteLock(); + } + } + /**** ** Sprite batch ****/ -- cgit From 609ceedaba6cd42e7025f1a15ebfeb26c69bab80 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 11 Jan 2020 16:53:58 -0500 Subject: update for compatibility list change --- src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs | 12 ++---------- src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModEntry.cs | 4 ++-- src/SMAPI.Web/Framework/Caching/Wiki/CachedWikiMod.cs | 8 ++++---- src/SMAPI.Web/ViewModels/ModModel.cs | 7 +++---- src/SMAPI.Web/Views/Mods/Index.cshtml | 7 ++----- 5 files changed, 13 insertions(+), 25 deletions(-) (limited to 'src') diff --git a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs index 384f23fc..c829c0f4 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiClient.cs @@ -102,6 +102,7 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki string anchor = this.GetAttribute(node, "id"); string contentPackFor = this.GetAttribute(node, "data-content-pack-for"); string devNote = this.GetAttribute(node, "data-dev-note"); + string pullRequestUrl = this.GetAttribute(node, "data-pr"); IDictionary mapLocalVersions = this.GetAttributeAsVersionMapping(node, "data-map-local-versions"); IDictionary mapRemoteVersions = this.GetAttributeAsVersionMapping(node, "data-map-remote-versions"); @@ -132,15 +133,6 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki } } - // parse links - List> metadataLinks = new List>(); - foreach (HtmlNode linkElement in node.Descendants("td").Last().Descendants("a").Skip(1)) // skip anchor link - { - string text = linkElement.InnerText.Trim(); - Uri url = new Uri(linkElement.GetAttributeValue("href", "")); - metadataLinks.Add(Tuple.Create(url, text)); - } - // yield model yield return new WikiModEntry { @@ -159,7 +151,7 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki Compatibility = compatibility, BetaCompatibility = betaCompatibility, Warnings = warnings, - MetadataLinks = metadataLinks.ToArray(), + PullRequestUrl = pullRequestUrl, DevNote = devNote, MapLocalVersions = mapLocalVersions, MapRemoteVersions = mapRemoteVersions, diff --git a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModEntry.cs b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModEntry.cs index 931dcd43..474dce3d 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModEntry.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/Wiki/WikiModEntry.cs @@ -57,8 +57,8 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.Wiki /// The human-readable warnings for players about this mod. public string[] Warnings { get; set; } - /// Extra metadata links (usually for open pull requests). - public Tuple[] MetadataLinks { get; set; } + /// The URL of the pull request which submits changes for an unofficial update to the author, if any. + public string PullRequestUrl { get; set; } /// Special notes intended for developers who maintain unofficial updates or submit pull requests. public string DevNote { get; set; } diff --git a/src/SMAPI.Web/Framework/Caching/Wiki/CachedWikiMod.cs b/src/SMAPI.Web/Framework/Caching/Wiki/CachedWikiMod.cs index 8569984a..7e7c99bc 100644 --- a/src/SMAPI.Web/Framework/Caching/Wiki/CachedWikiMod.cs +++ b/src/SMAPI.Web/Framework/Caching/Wiki/CachedWikiMod.cs @@ -67,8 +67,8 @@ namespace StardewModdingAPI.Web.Framework.Caching.Wiki /// The human-readable warnings for players about this mod. public string[] Warnings { get; set; } - /// Extra metadata links (usually for open pull requests). - public Tuple[] MetadataLinks { get; set; } + /// The URL of the pull request which submits changes for an unofficial update to the author, if any. + public string PullRequestUrl { get; set; } /// Special notes intended for developers who maintain unofficial updates or submit pull requests. public string DevNote { get; set; } @@ -150,7 +150,7 @@ namespace StardewModdingAPI.Web.Framework.Caching.Wiki this.CustomSourceUrl = mod.CustomSourceUrl; this.CustomUrl = mod.CustomUrl; this.ContentPackFor = mod.ContentPackFor; - this.MetadataLinks = mod.MetadataLinks; + this.PullRequestUrl = mod.PullRequestUrl; this.Warnings = mod.Warnings; this.DevNote = mod.DevNote; this.Anchor = mod.Anchor; @@ -192,7 +192,7 @@ namespace StardewModdingAPI.Web.Framework.Caching.Wiki CustomUrl = this.CustomUrl, ContentPackFor = this.ContentPackFor, Warnings = this.Warnings, - MetadataLinks = this.MetadataLinks, + PullRequestUrl = this.PullRequestUrl, DevNote = this.DevNote, Anchor = this.Anchor, diff --git a/src/SMAPI.Web/ViewModels/ModModel.cs b/src/SMAPI.Web/ViewModels/ModModel.cs index 2b478c81..56316ab7 100644 --- a/src/SMAPI.Web/ViewModels/ModModel.cs +++ b/src/SMAPI.Web/ViewModels/ModModel.cs @@ -1,4 +1,3 @@ -using System; using System.Collections.Generic; using System.Linq; using StardewModdingAPI.Toolkit.Framework.Clients.Wiki; @@ -38,8 +37,8 @@ namespace StardewModdingAPI.Web.ViewModels /// The human-readable warnings for players about this mod. public string[] Warnings { get; set; } - /// Extra metadata links (usually for open pull requests). - public Tuple[] MetadataLinks { get; set; } + /// The URL of the pull request which submits changes for an unofficial update to the author, if any. + public string PullRequestUrl { get; set; } /// Special notes intended for developers who maintain unofficial updates or submit pull requests. public string DevNote { get; set; } @@ -68,7 +67,7 @@ namespace StardewModdingAPI.Web.ViewModels this.BetaCompatibility = entry.BetaCompatibility != null ? new ModCompatibilityModel(entry.BetaCompatibility) : null; this.ModPages = this.GetModPageUrls(entry).ToArray(); this.Warnings = entry.Warnings; - this.MetadataLinks = entry.MetadataLinks; + this.PullRequestUrl = entry.PullRequestUrl; this.DevNote = entry.DevNote; this.Slug = entry.Anchor; } diff --git a/src/SMAPI.Web/Views/Mods/Index.cshtml b/src/SMAPI.Web/Views/Mods/Index.cshtml index 50b59b45..5b310d55 100644 --- a/src/SMAPI.Web/Views/Mods/Index.cshtml +++ b/src/SMAPI.Web/Views/Mods/Index.cshtml @@ -110,11 +110,8 @@ else # - - - [dev note] + PR + [dev note] -- cgit From bffc7f28e9abcdb2a3e5e8ee5353f4c2f7e111a0 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 11 Jan 2020 19:16:46 -0500 Subject: fix update-check error for Chucklefish pages with no version --- docs/release-notes.md | 1 + src/SMAPI.Web/Framework/Clients/Chucklefish/ChucklefishClient.cs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index dd4158aa..22e609dd 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -7,6 +7,7 @@ * SMAPI now prevents mods from crashing the game with invalid schedule data. * Updated minimum game version (1.4 → 1.4.1). * Fixed 'collection was modified' error when returning to title in rare cases. + * Fixed update-check error if a mod's Chucklefish page has no version. ## 3.1 Released 05 January 2019 for Stardew Valley 1.4 or later. diff --git a/src/SMAPI.Web/Framework/Clients/Chucklefish/ChucklefishClient.cs b/src/SMAPI.Web/Framework/Clients/Chucklefish/ChucklefishClient.cs index 939c32c6..cdb281e2 100644 --- a/src/SMAPI.Web/Framework/Clients/Chucklefish/ChucklefishClient.cs +++ b/src/SMAPI.Web/Framework/Clients/Chucklefish/ChucklefishClient.cs @@ -59,7 +59,7 @@ namespace StardewModdingAPI.Web.Framework.Clients.Chucklefish string name = doc.DocumentNode.SelectSingleNode("//meta[@name='twitter:title']").Attributes["content"].Value; if (name.StartsWith("[SMAPI] ")) name = name.Substring("[SMAPI] ".Length); - string version = doc.DocumentNode.SelectSingleNode("//h1/span").InnerText; + string version = doc.DocumentNode.SelectSingleNode("//h1/span")?.InnerText; // create model return new ChucklefishMod -- cgit From 2b68be4ebbb16213167b9bd04e891c7e05c67400 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 11 Jan 2020 20:33:01 -0500 Subject: add version mappings from the wiki to API data --- docs/release-notes.md | 3 +++ .../Framework/Clients/WebApi/ModExtendedMetadataModel.cs | 16 ++++++++++++++-- 2 files changed, 17 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index 22e609dd..63cbbc2c 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -9,6 +9,9 @@ * Fixed 'collection was modified' error when returning to title in rare cases. * Fixed update-check error if a mod's Chucklefish page has no version. +* For SMAPI/tool developers: + * The `/mods` web API endpoint now includes version mappings from the wiki. + ## 3.1 Released 05 January 2019 for Stardew Valley 1.4 or later. diff --git a/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModExtendedMetadataModel.cs b/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModExtendedMetadataModel.cs index 4a697585..8c21e4e0 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModExtendedMetadataModel.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModExtendedMetadataModel.cs @@ -55,7 +55,7 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi /// The latest unofficial version, if newer than and . public ModEntryVersionModel Unofficial { get; set; } - /// The latest unofficial version for the current Stardew Valley or SMAPI beta, if any (see ). + /// The latest unofficial version for the current Stardew Valley or SMAPI beta, if any. public ModEntryVersionModel UnofficialForBeta { get; set; } /**** @@ -84,6 +84,15 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi /// The beta game or SMAPI version which broke this mod, if applicable. public string BetaBrokeIn { get; set; } + /**** + ** Version mappings + ****/ + /// Maps local versions to a semantic version for update checks. + public IDictionary MapLocalVersions { get; set; } + + /// Maps remote versions to a semantic version for update checks. + public IDictionary MapRemoteVersions { get; set; } + /********* ** Public methods @@ -127,13 +136,16 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi this.BetaCompatibilityStatus = wiki.BetaCompatibility?.Status; this.BetaCompatibilitySummary = wiki.BetaCompatibility?.Summary; this.BetaBrokeIn = wiki.BetaCompatibility?.BrokeIn; + + this.MapLocalVersions = wiki.MapLocalVersions; + this.MapRemoteVersions = wiki.MapRemoteVersions; } // internal DB data if (db != null) { this.ID = this.ID.Union(db.FormerIDs).ToArray(); - this.Name = this.Name ?? db.DisplayName; + this.Name ??= db.DisplayName; } } -- cgit From 351f5ad5e0ec7f0fe703888b7fd5a3770a330e15 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 12 Jan 2020 14:33:07 -0500 Subject: fix recursive lock error --- src/SMAPI/Framework/ContentCoordinator.cs | 48 +++++++++++++++---------------- 1 file changed, 23 insertions(+), 25 deletions(-) (limited to 'src') diff --git a/src/SMAPI/Framework/ContentCoordinator.cs b/src/SMAPI/Framework/ContentCoordinator.cs index 0e62837c..b2bacea6 100644 --- a/src/SMAPI/Framework/ContentCoordinator.cs +++ b/src/SMAPI/Framework/ContentCoordinator.cs @@ -193,16 +193,15 @@ namespace StardewModdingAPI.Framework /// The internal SMAPI asset key. public T LoadManagedAsset(string contentManagerID, string relativePath) { - return this.ContentManagerLock.InReadLock(() => - { - // get content manager - IContentManager contentManager = this.ContentManagers.FirstOrDefault(p => p.IsNamespaced && p.Name == contentManagerID); - if (contentManager == null) - throw new InvalidOperationException($"The '{contentManagerID}' prefix isn't handled by any mod."); + // get content manager + IContentManager contentManager = this.ContentManagerLock.InReadLock(() => + this.ContentManagers.FirstOrDefault(p => p.IsNamespaced && p.Name == contentManagerID) + ); + if (contentManager == null) + throw new InvalidOperationException($"The '{contentManagerID}' prefix isn't handled by any mod."); - // get fresh asset - return contentManager.Load(relativePath, this.DefaultLanguage, useCache: false); - }); + // get fresh asset + return contentManager.Load(relativePath, this.DefaultLanguage, useCache: false); } /// Purge matched assets from the cache. @@ -225,10 +224,10 @@ namespace StardewModdingAPI.Framework /// Returns the invalidated asset names. public IEnumerable InvalidateCache(Func predicate, bool dispose = false) { - return this.ContentManagerLock.InReadLock(() => + // invalidate cache & track removed assets + IDictionary> removedAssets = new Dictionary>(StringComparer.InvariantCultureIgnoreCase); + this.ContentManagerLock.InReadLock(() => { - // invalidate cache & track removed assets - IDictionary> removedAssets = new Dictionary>(StringComparer.InvariantCultureIgnoreCase); foreach (IContentManager contentManager in this.ContentManagers) { foreach (var entry in contentManager.InvalidateCache(predicate, dispose)) @@ -238,18 +237,18 @@ namespace StardewModdingAPI.Framework assets.Add(entry.Value); } } + }); - // reload core game assets - if (removedAssets.Any()) - { - IDictionary propagated = this.CoreAssets.Propagate(this.MainContentManager, removedAssets.ToDictionary(p => p.Key, p => p.Value.First().GetType())); // use an intercepted content manager - this.Monitor.Log($"Invalidated {removedAssets.Count} asset names ({string.Join(", ", removedAssets.Keys.OrderBy(p => p, StringComparer.InvariantCultureIgnoreCase))}); propagated {propagated.Count(p => p.Value)} core assets.", LogLevel.Trace); - } - else - this.Monitor.Log("Invalidated 0 cache entries.", LogLevel.Trace); + // reload core game assets + if (removedAssets.Any()) + { + IDictionary propagated = this.CoreAssets.Propagate(this.MainContentManager, removedAssets.ToDictionary(p => p.Key, p => p.Value.First().GetType())); // use an intercepted content manager + this.Monitor.Log($"Invalidated {removedAssets.Count} asset names ({string.Join(", ", removedAssets.Keys.OrderBy(p => p, StringComparer.InvariantCultureIgnoreCase))}); propagated {propagated.Count(p => p.Value)} core assets.", LogLevel.Trace); + } + else + this.Monitor.Log("Invalidated 0 cache entries.", LogLevel.Trace); - return removedAssets.Keys; - }); + return removedAssets.Keys; } /// Dispose held resources. @@ -280,9 +279,8 @@ namespace StardewModdingAPI.Framework return; this.ContentManagerLock.InWriteLock(() => - { - this.ContentManagers.Remove(contentManager); - }); + this.ContentManagers.Remove(contentManager) + ); } } } -- cgit From 5518e4cf241e487da26bd2e651a57724389edfe2 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 12 Jan 2020 15:45:54 -0500 Subject: fix asset propagation for player sprites not affecting other players or recolor maps --- docs/release-notes.md | 3 +++ src/SMAPI/Metadata/CoreAssetPropagator.cs | 33 +++++++++++++++++++++---------- 2 files changed, 26 insertions(+), 10 deletions(-) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index 63cbbc2c..f5c49a88 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -9,6 +9,9 @@ * Fixed 'collection was modified' error when returning to title in rare cases. * Fixed update-check error if a mod's Chucklefish page has no version. +For modders: + * Asset propagation for player sprites now affects other players' sprites, and updates recolor maps (e.g. sleeves). + * For SMAPI/tool developers: * The `/mods` web API endpoint now includes version mappings from the wiki. diff --git a/src/SMAPI/Metadata/CoreAssetPropagator.cs b/src/SMAPI/Metadata/CoreAssetPropagator.cs index b86a6790..abe28ce9 100644 --- a/src/SMAPI/Metadata/CoreAssetPropagator.cs +++ b/src/SMAPI/Metadata/CoreAssetPropagator.cs @@ -190,17 +190,9 @@ namespace StardewModdingAPI.Metadata case "characters\\farmer\\farmer_base": // Farmer case "characters\\farmer\\farmer_base_bald": - if (Game1.player == null || !Game1.player.IsMale) - return false; - Game1.player.FarmerRenderer = new FarmerRenderer(key, Game1.player); - return true; - - case "characters\\farmer\\farmer_girl_base": // Farmer + case "characters\\farmer\\farmer_girl_base": case "characters\\farmer\\farmer_girl_base_bald": - if (Game1.player == null || Game1.player.IsMale) - return false; - Game1.player.FarmerRenderer = new FarmerRenderer(key, Game1.player); - return true; + return this.ReloadPlayerSprites(key); case "characters\\farmer\\hairstyles": // Game1.LoadContent FarmerRenderer.hairStylesTexture = content.Load(key); @@ -835,6 +827,27 @@ namespace StardewModdingAPI.Metadata } } + /// Reload the sprites for matching players. + /// The asset key to reload. + private bool ReloadPlayerSprites(string key) + { + Farmer[] players = + ( + from player in Game1.getOnlineFarmers() + where key == this.NormalizeAssetNameIgnoringEmpty(player.getTexture()) + select player + ) + .ToArray(); + + foreach (Farmer player in players) + { + this.Reflection.GetField>>>(typeof(FarmerRenderer), "_recolorOffsets").GetValue().Remove(player.getTexture()); + player.FarmerRenderer.MarkSpriteDirty(); + } + + return players.Any(); + } + /// Reload tree textures. /// The content manager through which to reload the asset. /// The asset key to reload. -- cgit From 8b1fd90c6e72bff99d81a3b9614fdeaa6f67a950 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 12 Jan 2020 15:53:59 -0500 Subject: remove invalid-schedule error which can have false positives (e.g. when NPC is married to a player) --- docs/release-notes.md | 3 ++- src/SMAPI/Metadata/CoreAssetPropagator.cs | 16 +++++++--------- 2 files changed, 9 insertions(+), 10 deletions(-) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index f5c49a88..bc30bed7 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -11,8 +11,9 @@ For modders: * Asset propagation for player sprites now affects other players' sprites, and updates recolor maps (e.g. sleeves). + * Removed invalid-schedule validation which had false positives. -* For SMAPI/tool developers: +For SMAPI/tool developers: * The `/mods` web API endpoint now includes version mappings from the wiki. ## 3.1 diff --git a/src/SMAPI/Metadata/CoreAssetPropagator.cs b/src/SMAPI/Metadata/CoreAssetPropagator.cs index abe28ce9..57e1d197 100644 --- a/src/SMAPI/Metadata/CoreAssetPropagator.cs +++ b/src/SMAPI/Metadata/CoreAssetPropagator.cs @@ -909,18 +909,16 @@ namespace StardewModdingAPI.Metadata this.Reflection.GetField(villager, "_hasLoadedMasterScheduleData").SetValue(false); this.Reflection.GetField>(villager, "_masterScheduleData").SetValue(null); villager.Schedule = villager.getSchedule(Game1.dayOfMonth); - if (villager.Schedule == null) - { - this.Monitor.Log($"A mod set an invalid schedule for {villager.Name ?? key}, so the NPC may not behave correctly.", LogLevel.Warn); - return true; - } // switch to new schedule if needed - int lastScheduleTime = villager.Schedule.Keys.Where(p => p <= Game1.timeOfDay).OrderByDescending(p => p).FirstOrDefault(); - if (lastScheduleTime != 0) + if (villager.Schedule != null) { - villager.scheduleTimeToTry = NPC.NO_TRY; // use time that's passed in to checkSchedule - villager.checkSchedule(lastScheduleTime); + int lastScheduleTime = villager.Schedule.Keys.Where(p => p <= Game1.timeOfDay).OrderByDescending(p => p).FirstOrDefault(); + if (lastScheduleTime != 0) + { + villager.scheduleTimeToTry = NPC.NO_TRY; // use time that's passed in to checkSchedule + villager.checkSchedule(lastScheduleTime); + } } } return true; -- cgit From 734c727bc3629986feb5cb331a6e4e8a6b20391b Mon Sep 17 00:00:00 2001 From: danvolchek Date: Sun, 12 Jan 2020 15:24:57 -0800 Subject: set updateFound based on the suggested update response --- src/SMAPI/Framework/SCore.cs | 2 ++ 1 file changed, 2 insertions(+) (limited to 'src') diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index c4841ece..b80f8ddf 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -590,6 +590,8 @@ namespace StardewModdingAPI.Framework else this.Monitor.Log(" SMAPI okay.", LogLevel.Trace); + updateFound = response.SuggestedUpdate?.Version; + // show errors if (response.Errors.Any()) { -- cgit From d68e4f97665898027f45782b8e66333912ae08ea Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 12 Jan 2020 19:41:14 -0500 Subject: drop pre-3.0 update-check support --- docs/release-notes.md | 1 + .../Framework/Clients/WebApi/ModEntryModel.cs | 22 ---------------------- src/SMAPI.Web/Controllers/ModsApiController.cs | 17 ++--------------- 3 files changed, 3 insertions(+), 37 deletions(-) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index bc30bed7..e2c6d500 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -15,6 +15,7 @@ For modders: For SMAPI/tool developers: * The `/mods` web API endpoint now includes version mappings from the wiki. + * Dropped API support for the pre-3.0 update-check format. ## 3.1 Released 05 January 2019 for Stardew Valley 1.4 or later. diff --git a/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModEntryModel.cs b/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModEntryModel.cs index f1bcfccc..2f58a3f1 100644 --- a/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModEntryModel.cs +++ b/src/SMAPI.Toolkit/Framework/Clients/WebApi/ModEntryModel.cs @@ -1,5 +1,3 @@ -using System; - namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi { /// Metadata about a mod. @@ -17,26 +15,6 @@ namespace StardewModdingAPI.Toolkit.Framework.Clients.WebApi /// Optional extended data which isn't needed for update checks. public ModExtendedMetadataModel Metadata { get; set; } - /// The main version. - [Obsolete] - public ModEntryVersionModel Main { get; set; } - - /// The latest optional version, if newer than . - [Obsolete] - public ModEntryVersionModel Optional { get; set; } - - /// The latest unofficial version, if newer than and . - [Obsolete] - public ModEntryVersionModel Unofficial { get; set; } - - /// The latest unofficial version for the current Stardew Valley or SMAPI beta, if any (see ). - [Obsolete] - public ModEntryVersionModel UnofficialForBeta { get; set; } - - /// Whether a Stardew Valley or SMAPI beta which affects mod compatibility is in progress. If this is true, should be used for beta versions of SMAPI instead of . - [Obsolete] - public bool? HasBetaInfo { get; set; } - /// The errors that occurred while fetching update data. public string[] Errors { get; set; } = new string[0]; } diff --git a/src/SMAPI.Web/Controllers/ModsApiController.cs b/src/SMAPI.Web/Controllers/ModsApiController.cs index 3e3b81c8..f194b4d0 100644 --- a/src/SMAPI.Web/Controllers/ModsApiController.cs +++ b/src/SMAPI.Web/Controllers/ModsApiController.cs @@ -94,8 +94,6 @@ namespace StardewModdingAPI.Web.Controllers if (model?.Mods == null) return new ModEntryModel[0]; - bool legacyMode = SemanticVersion.TryParse(version, out ISemanticVersion parsedVersion) && parsedVersion.IsOlderThan("3.0.0-beta.20191109"); - // fetch wiki data WikiModEntry[] wikiData = this.WikiCache.GetWikiMods().Select(p => p.GetModel()).ToArray(); IDictionary mods = new Dictionary(StringComparer.CurrentCultureIgnoreCase); @@ -104,19 +102,8 @@ namespace StardewModdingAPI.Web.Controllers if (string.IsNullOrWhiteSpace(mod.ID)) continue; - ModEntryModel result = await this.GetModData(mod, wikiData, model.IncludeExtendedMetadata || legacyMode, model.ApiVersion); - if (legacyMode) - { - result.Main = result.Metadata.Main; - result.Optional = result.Metadata.Optional; - result.Unofficial = result.Metadata.Unofficial; - result.UnofficialForBeta = result.Metadata.UnofficialForBeta; - result.HasBetaInfo = result.Metadata.BetaCompatibilityStatus != null; - result.SuggestedUpdate = null; - if (!model.IncludeExtendedMetadata) - result.Metadata = null; - } - else if (!model.IncludeExtendedMetadata && (model.ApiVersion == null || mod.InstalledVersion == null)) + ModEntryModel result = await this.GetModData(mod, wikiData, model.IncludeExtendedMetadata, model.ApiVersion); + if (!model.IncludeExtendedMetadata && (model.ApiVersion == null || mod.InstalledVersion == null)) { var errors = new List(result.Errors); errors.Add($"This API can't suggest an update because {nameof(model.ApiVersion)} or {nameof(mod.InstalledVersion)} are null, and you didn't specify {nameof(model.IncludeExtendedMetadata)} to get other info. See the SMAPI technical docs for usage."); -- cgit From 694cca4b21878850ba6131105a0c560fdfbc5f10 Mon Sep 17 00:00:00 2001 From: Drachenkaetzchen Date: Wed, 15 Jan 2020 16:01:35 +0100 Subject: Added documentation for all performance counter methods and members. Refactored the naming of several members and methods to reflect their actual intention. --- .../Commands/Other/PerformanceCounterCommand.cs | 30 ++-- .../Framework/PerformanceCounter/AlertContext.cs | 18 ++- .../Framework/PerformanceCounter/AlertEntry.cs | 25 +++- .../EventPerformanceCounterCollection.cs | 5 + .../PerformanceCounter/IPerformanceCounterEvent.cs | 15 -- .../PerformanceCounter/PerformanceCounter.cs | 99 ++++++------- .../PerformanceCounterCollection.cs | 123 +++++++++------- .../PerformanceCounter/PerformanceCounterEntry.cs | 10 +- .../PerformanceCounterManager.cs | 157 ++++++++++++--------- src/SMAPI/Framework/SCore.cs | 2 +- src/SMAPI/Framework/SGame.cs | 2 +- 11 files changed, 276 insertions(+), 210 deletions(-) delete mode 100644 src/SMAPI/Framework/PerformanceCounter/IPerformanceCounterEvent.cs (limited to 'src') diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/PerformanceCounterCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/PerformanceCounterCommand.cs index 84b9504e..750e3792 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/PerformanceCounterCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/PerformanceCounterCommand.cs @@ -134,8 +134,8 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other { if (sourceName == null) { - collection.Monitor = true; - collection.MonitorThresholdMilliseconds = threshold; + collection.EnableAlerts = true; + collection.AlertThresholdMilliseconds = threshold; monitor.Log($"Set up monitor for '{collectionName}' with '{this.FormatMilliseconds(threshold)}'", LogLevel.Info); return; } @@ -145,8 +145,8 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other { if (performanceCounter.Value.Source.ToLowerInvariant().Equals(sourceName.ToLowerInvariant())) { - performanceCounter.Value.Monitor = true; - performanceCounter.Value.MonitorThresholdMilliseconds = threshold; + performanceCounter.Value.EnableAlerts = true; + performanceCounter.Value.AlertThresholdMilliseconds = threshold; monitor.Log($"Set up monitor for '{sourceName}' in collection '{collectionName}' with '{this.FormatMilliseconds(threshold)}", LogLevel.Info); return; } @@ -167,17 +167,17 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other int clearedCounters = 0; foreach (PerformanceCounterCollection collection in SCore.PerformanceCounterManager.PerformanceCounterCollections) { - if (collection.Monitor) + if (collection.EnableAlerts) { - collection.Monitor = false; + collection.EnableAlerts = false; clearedCounters++; } foreach (var performanceCounter in collection.PerformanceCounters) { - if (performanceCounter.Value.Monitor) + if (performanceCounter.Value.EnableAlerts) { - performanceCounter.Value.Monitor = false; + performanceCounter.Value.EnableAlerts = false; clearedCounters++; } } @@ -197,14 +197,14 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other foreach (PerformanceCounterCollection collection in SCore.PerformanceCounterManager.PerformanceCounterCollections) { - if (collection.Monitor) + if (collection.EnableAlerts) { - collectionMonitors.Add((collection.Name, collection.MonitorThresholdMilliseconds)); + collectionMonitors.Add((collection.Name, collection.AlertThresholdMilliseconds)); } sourceMonitors.AddRange(from performanceCounter in - collection.PerformanceCounters where performanceCounter.Value.Monitor - select (collection.Name, performanceCounter.Value.Source, performanceCounter.Value.MonitorThresholdMilliseconds)); + collection.PerformanceCounters where performanceCounter.Value.EnableAlerts + select (collection.Name, performanceCounter.Value.Source, MonitorThresholdMilliseconds: performanceCounter.Value.AlertThresholdMilliseconds)); } if (collectionMonitors.Count > 0) @@ -377,7 +377,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other switch (type) { case "category": - SCore.PerformanceCounterManager.ResetCategory(name); + SCore.PerformanceCounterManager.ResetCollection(name); monitor.Log($"All performance counters for category {name} are now cleared.", LogLevel.Info); break; case "mod": @@ -491,8 +491,8 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other { item.Key, this.FormatMilliseconds(item.Value.GetAverage(averageInterval), thresholdMilliseconds), - this.FormatMilliseconds(item.Value.GetLastEntry()?.Elapsed.TotalMilliseconds), - this.FormatMilliseconds(item.Value.GetPeak()?.Elapsed.TotalMilliseconds) + this.FormatMilliseconds(item.Value.GetLastEntry()?.ElapsedMilliseconds), + this.FormatMilliseconds(item.Value.GetPeak()?.ElapsedMilliseconds) } )); diff --git a/src/SMAPI/Framework/PerformanceCounter/AlertContext.cs b/src/SMAPI/Framework/PerformanceCounter/AlertContext.cs index c4a57a49..63f0a5ed 100644 --- a/src/SMAPI/Framework/PerformanceCounter/AlertContext.cs +++ b/src/SMAPI/Framework/PerformanceCounter/AlertContext.cs @@ -1,14 +1,26 @@ namespace StardewModdingAPI.Framework.PerformanceCounter { - public struct AlertContext + /// The context for an alert. + internal struct AlertContext { - public string Source; - public double Elapsed; + /// The source which triggered the alert. + public readonly string Source; + /// The elapsed milliseconds. + public readonly double Elapsed; + + /// Creates a new alert context. + /// The source which triggered the alert. + /// The elapsed milliseconds. public AlertContext(string source, double elapsed) { this.Source = source; this.Elapsed = elapsed; } + + public override string ToString() + { + return $"{this.Source}: {this.Elapsed:F2}ms"; + } } } diff --git a/src/SMAPI/Framework/PerformanceCounter/AlertEntry.cs b/src/SMAPI/Framework/PerformanceCounter/AlertEntry.cs index 284af1ce..b87d8642 100644 --- a/src/SMAPI/Framework/PerformanceCounter/AlertEntry.cs +++ b/src/SMAPI/Framework/PerformanceCounter/AlertEntry.cs @@ -2,18 +2,31 @@ using System.Collections.Generic; namespace StardewModdingAPI.Framework.PerformanceCounter { + /// A single alert entry. internal struct AlertEntry { - public PerformanceCounterCollection Collection; - public double ExecutionTimeMilliseconds; - public double Threshold; - public List Context; + /// The collection in which the alert occurred. + public readonly PerformanceCounterCollection Collection; - public AlertEntry(PerformanceCounterCollection collection, double executionTimeMilliseconds, double threshold, List context) + /// The actual execution time in milliseconds. + public readonly double ExecutionTimeMilliseconds; + + /// The configured alert threshold. + public readonly double ThresholdMilliseconds; + + /// The context list, which records all sources involved in exceeding the threshold. + public readonly List Context; + + /// Creates a new alert entry. + /// The source collection in which the alert occurred. + /// The actual execution time in milliseconds. + /// The configured threshold in milliseconds. + /// A list of AlertContext to record which sources were involved + public AlertEntry(PerformanceCounterCollection collection, double executionTimeMilliseconds, double thresholdMilliseconds, List context) { this.Collection = collection; this.ExecutionTimeMilliseconds = executionTimeMilliseconds; - this.Threshold = threshold; + this.ThresholdMilliseconds = thresholdMilliseconds; this.Context = context; } } diff --git a/src/SMAPI/Framework/PerformanceCounter/EventPerformanceCounterCollection.cs b/src/SMAPI/Framework/PerformanceCounter/EventPerformanceCounterCollection.cs index 1aec28f3..4690c512 100644 --- a/src/SMAPI/Framework/PerformanceCounter/EventPerformanceCounterCollection.cs +++ b/src/SMAPI/Framework/PerformanceCounter/EventPerformanceCounterCollection.cs @@ -2,8 +2,13 @@ using StardewModdingAPI.Framework.Events; namespace StardewModdingAPI.Framework.PerformanceCounter { + /// Represents a performance counter collection specific to game events. internal class EventPerformanceCounterCollection: PerformanceCounterCollection { + /// Creates a new event performance counter collection. + /// The performance counter manager. + /// The ManagedEvent. + /// If the event is flagged as important. public EventPerformanceCounterCollection(PerformanceCounterManager manager, IManagedEvent @event, bool isImportant) : base(manager, @event.GetName(), isImportant) { } diff --git a/src/SMAPI/Framework/PerformanceCounter/IPerformanceCounterEvent.cs b/src/SMAPI/Framework/PerformanceCounter/IPerformanceCounterEvent.cs deleted file mode 100644 index 1bcf4fa0..00000000 --- a/src/SMAPI/Framework/PerformanceCounter/IPerformanceCounterEvent.cs +++ /dev/null @@ -1,15 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace StardewModdingAPI.Framework.Utilities -{ - public interface IPerformanceCounterEvent - { - string GetEventName(); - long GetAverageCallsPerSecond(); - - double GetGameAverageExecutionTime(); - double GetModsAverageExecutionTime(); - double GetAverageExecutionTime(); - } -} diff --git a/src/SMAPI/Framework/PerformanceCounter/PerformanceCounter.cs b/src/SMAPI/Framework/PerformanceCounter/PerformanceCounter.cs index 3dbc693a..b2ec4c90 100644 --- a/src/SMAPI/Framework/PerformanceCounter/PerformanceCounter.cs +++ b/src/SMAPI/Framework/PerformanceCounter/PerformanceCounter.cs @@ -1,26 +1,32 @@ using System; -using System.Diagnostics; using System.Linq; using Cyotek.Collections.Generic; -using StardewModdingAPI.Framework.Utilities; namespace StardewModdingAPI.Framework.PerformanceCounter { internal class PerformanceCounter { + /// The size of the ring buffer. private const int MAX_ENTRIES = 16384; - public string Source { get; } - public static Stopwatch Stopwatch = new Stopwatch(); - public static long TotalNumEventsLogged; - public double MonitorThresholdMilliseconds { get; set; } - public bool Monitor { get; set; } + /// The collection to which this performance counter belongs. private readonly PerformanceCounterCollection ParentCollection; + /// The circular buffer which stores all performance counter entries private readonly CircularBuffer _counter; + /// The peak execution time private PerformanceCounterEntry? PeakPerformanceCounterEntry; + /// The name of the source. + public string Source { get; } + + /// The alert threshold in milliseconds + public double AlertThresholdMilliseconds { get; set; } + + /// If alerting is enabled or not + public bool EnableAlerts { get; set; } + public PerformanceCounter(PerformanceCounterCollection parentCollection, string source) { this.ParentCollection = parentCollection; @@ -28,90 +34,87 @@ namespace StardewModdingAPI.Framework.PerformanceCounter this._counter = new CircularBuffer(PerformanceCounter.MAX_ENTRIES); } - public void Reset() - { - this._counter.Clear(); - this.PeakPerformanceCounterEntry = null; - } - - public int GetAverageCallsPerSecond() - { - var x = this._counter.GroupBy( - p => - (int) p.EventTime.Subtract( - new DateTime(1970, 1, 1) - ).TotalSeconds); - - return x.Last().Count(); - } - + /// Adds a new performance counter entry to the list. Updates the peak entry and adds an alert if + /// monitoring is enabled and the execution time exceeds the threshold. + /// The entry to add. public void Add(PerformanceCounterEntry entry) { - PerformanceCounter.Stopwatch.Start(); this._counter.Put(entry); - if (this.Monitor && entry.Elapsed.TotalMilliseconds > this.MonitorThresholdMilliseconds) - { - this.ParentCollection.AddAlert(entry.Elapsed.TotalMilliseconds, this.MonitorThresholdMilliseconds, new AlertContext(this.Source, entry.Elapsed.TotalMilliseconds)); - } + if (this.EnableAlerts && entry.ElapsedMilliseconds > this.AlertThresholdMilliseconds) + this.ParentCollection.AddAlert(entry.ElapsedMilliseconds, this.AlertThresholdMilliseconds, + new AlertContext(this.Source, entry.ElapsedMilliseconds)); if (this.PeakPerformanceCounterEntry == null) - { this.PeakPerformanceCounterEntry = entry; - } else { - if (entry.Elapsed.TotalMilliseconds > this.PeakPerformanceCounterEntry.Value.Elapsed.TotalMilliseconds) - { + if (entry.ElapsedMilliseconds > this.PeakPerformanceCounterEntry.Value.ElapsedMilliseconds) this.PeakPerformanceCounterEntry = entry; - } } + } - PerformanceCounter.Stopwatch.Stop(); - PerformanceCounter.TotalNumEventsLogged++; + /// Clears all performance counter entries and resets the peak entry. + public void Reset() + { + this._counter.Clear(); + this.PeakPerformanceCounterEntry = null; } + /// Returns the peak entry. + /// The peak entry. public PerformanceCounterEntry? GetPeak() { return this.PeakPerformanceCounterEntry; } + /// Resets the peak entry. public void ResetPeak() { this.PeakPerformanceCounterEntry = null; } + /// Returns the last entry added to the list. + /// The last entry public PerformanceCounterEntry? GetLastEntry() { if (this._counter.IsEmpty) - { return null; - } + return this._counter.PeekLast(); } + /// Returns the average execution time of all entries. + /// The average execution time in milliseconds. public double GetAverage() { if (this._counter.IsEmpty) - { return 0; - } - return this._counter.Average(p => p.Elapsed.TotalMilliseconds); + return this._counter.Average(p => p.ElapsedMilliseconds); } - public double GetAverage(TimeSpan range) + /// Returns the average over a given time span. + /// The time range to retrieve. + /// The DateTime from which to start the average. Defaults to DateTime.UtcNow if null + /// The average execution time in milliseconds. + /// + /// The relativeTo parameter specifies from which point in time the range is subtracted. Example: + /// If DateTime is set to 60 seconds ago, and the range is set to 60 seconds, the method would return + /// the average between all entries between 120s ago and 60s ago. + /// + public double GetAverage(TimeSpan range, DateTime? relativeTo = null) { if (this._counter.IsEmpty) - { return 0; - } - var lastTime = this._counter.Max(x => x.EventTime); - var start = lastTime.Subtract(range); + if (relativeTo == null) + relativeTo = DateTime.UtcNow; + + DateTime start = relativeTo.Value.Subtract(range); - var entries = this._counter.Where(x => (x.EventTime >= start) && (x.EventTime <= lastTime)); - return entries.Average(x => x.Elapsed.TotalMilliseconds); + var entries = this._counter.Where(x => (x.EventTime >= start) && (x.EventTime <= relativeTo)); + return entries.Average(x => x.ElapsedMilliseconds); } } } diff --git a/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterCollection.cs b/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterCollection.cs index 343fddf6..b48efd67 100644 --- a/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterCollection.cs +++ b/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterCollection.cs @@ -2,23 +2,41 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; -using StardewModdingAPI.Framework.Utilities; namespace StardewModdingAPI.Framework.PerformanceCounter { internal class PerformanceCounterCollection { - public IDictionary PerformanceCounters { get; } = new Dictionary(); - private DateTime StartDateTime = DateTime.Now; - private long CallCount; - public string Name { get; private set; } - public bool IsImportant { get; set; } - private readonly Stopwatch Stopwatch = new Stopwatch(); - private readonly PerformanceCounterManager PerformanceCounterManager; - public double MonitorThresholdMilliseconds { get; set; } - public bool Monitor { get; set; } + /// The list of triggered performance counters. private readonly List TriggeredPerformanceCounters = new List(); + /// The stopwatch used to track the invocation time. + private readonly Stopwatch InvocationStopwatch = new Stopwatch(); + + /// The performance counter manager. + private readonly PerformanceCounterManager PerformanceCounterManager; + + /// Holds the time to calculate the average calls per second. + private DateTime CallsPerSecondStart = DateTime.UtcNow; + + /// The number of invocations of this collection. + private long CallCount; + + public IDictionary PerformanceCounters { get; } = new Dictionary(); + + /// The name of this collection. + public string Name { get; } + + /// Flag if this collection is important (used for the console summary command). + public bool IsImportant { get; } + + /// The alert threshold in milliseconds. + public double AlertThresholdMilliseconds { get; set; } + + /// If alerting is enabled or not + public bool EnableAlerts { get; set; } + + public PerformanceCounterCollection(PerformanceCounterManager performanceCounterManager, string name, bool isImportant) { this.Name = name; @@ -32,111 +50,120 @@ namespace StardewModdingAPI.Framework.PerformanceCounter this.Name = name; } + /// Tracks a single invocation for a named source. + /// The name of the source. + /// The entry. public void Track(string source, PerformanceCounterEntry entry) { if (!this.PerformanceCounters.ContainsKey(source)) - { this.PerformanceCounters.Add(source, new PerformanceCounter(this, source)); - } + this.PerformanceCounters[source].Add(entry); - if (this.Monitor) - { - this.TriggeredPerformanceCounters.Add(new AlertContext(source, entry.Elapsed.TotalMilliseconds)); - } + if (this.EnableAlerts) + this.TriggeredPerformanceCounters.Add(new AlertContext(source, entry.ElapsedMilliseconds)); } + /// Returns the average execution time for all non-game internal sources. + /// The average execution time in milliseconds public double GetModsAverageExecutionTime() { - return this.PerformanceCounters.Where(p => p.Key != Constants.GamePerformanceCounterName).Sum(p => p.Value.GetAverage()); + return this.PerformanceCounters.Where(p => + p.Key != Constants.GamePerformanceCounterName).Sum(p => p.Value.GetAverage()); } + /// Returns the overall average execution time. + /// The average execution time in milliseconds public double GetAverageExecutionTime() { return this.PerformanceCounters.Sum(p => p.Value.GetAverage()); } + /// Returns the average execution time for game-internal sources. + /// The average execution time in milliseconds public double GetGameAverageExecutionTime() { if (this.PerformanceCounters.TryGetValue(Constants.GamePerformanceCounterName, out PerformanceCounter gameExecTime)) - { return gameExecTime.GetAverage(); - } return 0; } + /// Begins tracking the invocation of this collection. public void BeginTrackInvocation() { - if (this.Monitor) + if (this.EnableAlerts) { this.TriggeredPerformanceCounters.Clear(); - this.Stopwatch.Reset(); - this.Stopwatch.Start(); + this.InvocationStopwatch.Reset(); + this.InvocationStopwatch.Start(); } this.CallCount++; - } + /// Ends tracking the invocation of this collection. Also records an alert if alerting is enabled + /// and the invocation time exceeds the threshold. public void EndTrackInvocation() { - if (!this.Monitor) return; + if (!this.EnableAlerts) return; - this.Stopwatch.Stop(); - if (this.Stopwatch.Elapsed.TotalMilliseconds >= this.MonitorThresholdMilliseconds) - { - this.AddAlert(this.Stopwatch.Elapsed.TotalMilliseconds, - this.MonitorThresholdMilliseconds, this.TriggeredPerformanceCounters); - } + this.InvocationStopwatch.Stop(); + + if (this.InvocationStopwatch.Elapsed.TotalMilliseconds >= this.AlertThresholdMilliseconds) + this.AddAlert(this.InvocationStopwatch.Elapsed.TotalMilliseconds, + this.AlertThresholdMilliseconds, this.TriggeredPerformanceCounters); } - public void AddAlert(double executionTimeMilliseconds, double threshold, List alerts) + /// Adds an alert. + /// The execution time in milliseconds. + /// The configured threshold. + /// The list of alert contexts. + public void AddAlert(double executionTimeMilliseconds, double thresholdMilliseconds, List alerts) { this.PerformanceCounterManager.AddAlert(new AlertEntry(this, executionTimeMilliseconds, - threshold, alerts)); + thresholdMilliseconds, alerts)); } - public void AddAlert(double executionTimeMilliseconds, double threshold, AlertContext alert) + /// Adds an alert for a single AlertContext + /// The execution time in milliseconds. + /// The configured threshold. + /// The context + public void AddAlert(double executionTimeMilliseconds, double thresholdMilliseconds, AlertContext alert) { - this.AddAlert(executionTimeMilliseconds, threshold, new List() {alert}); + this.AddAlert(executionTimeMilliseconds, thresholdMilliseconds, new List() {alert}); } + /// Resets the calls per second counter. public void ResetCallsPerSecond() { this.CallCount = 0; - this.StartDateTime = DateTime.Now; + this.CallsPerSecondStart = DateTime.UtcNow; } + /// Resets all performance counters in this collection. public void Reset() { foreach (var i in this.PerformanceCounters) - { i.Value.Reset(); - i.Value.ResetPeak(); - } } + /// Resets the performance counter for a specific source. + /// The source name public void ResetSource(string source) { foreach (var i in this.PerformanceCounters) - { if (i.Value.Source.Equals(source, StringComparison.InvariantCultureIgnoreCase)) - { i.Value.Reset(); - i.Value.ResetPeak(); - } - } } + /// Returns the average calls per second. + /// The average calls per second. public long GetAverageCallsPerSecond() { - long runtimeInSeconds = (long) DateTime.Now.Subtract(this.StartDateTime).TotalSeconds; + long runtimeInSeconds = (long) DateTime.UtcNow.Subtract(this.CallsPerSecondStart).TotalSeconds; - if (runtimeInSeconds == 0) - { - return 0; - } + if (runtimeInSeconds == 0) return 0; return this.CallCount / runtimeInSeconds; } diff --git a/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterEntry.cs b/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterEntry.cs index 8e156a32..a50fce7d 100644 --- a/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterEntry.cs +++ b/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterEntry.cs @@ -1,10 +1,14 @@ using System; -namespace StardewModdingAPI.Framework.Utilities +namespace StardewModdingAPI.Framework.PerformanceCounter { - public struct PerformanceCounterEntry + /// A single performance counter entry. Records the DateTime of the event and the elapsed millisecond. + internal struct PerformanceCounterEntry { + /// The DateTime when the entry occured. public DateTime EventTime; - public TimeSpan Elapsed; + + /// The elapsed milliseconds + public double ElapsedMilliseconds; } } diff --git a/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterManager.cs b/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterManager.cs index 9e77e2fa..d8f1f172 100644 --- a/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterManager.cs +++ b/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterManager.cs @@ -4,72 +4,62 @@ using System.Diagnostics; using System.Linq; using System.Text; using StardewModdingAPI.Framework.Events; -using StardewModdingAPI.Framework.Utilities; namespace StardewModdingAPI.Framework.PerformanceCounter { internal class PerformanceCounterManager { public HashSet PerformanceCounterCollections = new HashSet(); - public List Alerts = new List(); + + /// The recorded alerts. + private readonly List Alerts = new List(); + + /// The monitor for output logging. private readonly IMonitor Monitor; - private readonly Stopwatch Stopwatch = new Stopwatch(); + /// The invocation stopwatch. + private readonly Stopwatch InvocationStopwatch = new Stopwatch(); + + /// Constructs a performance counter manager. + /// The monitor for output logging. public PerformanceCounterManager(IMonitor monitor) { this.Monitor = monitor; } + /// Resets all performance counters in all collections. public void Reset() { - foreach (var performanceCounter in this.PerformanceCounterCollections) - { - foreach (var eventPerformanceCounter in performanceCounter.PerformanceCounters) - { - eventPerformanceCounter.Value.Reset(); - } - } - } - - /// Print any queued messages. - public void PrintQueued() - { - if (this.Alerts.Count == 0) + foreach (var eventPerformanceCounter in + this.PerformanceCounterCollections.SelectMany(performanceCounter => performanceCounter.PerformanceCounters)) { - return; + eventPerformanceCounter.Value.Reset(); } - StringBuilder sb = new StringBuilder(); - - foreach (var alert in this.Alerts) - { - sb.AppendLine($"{alert.Collection.Name} took {alert.ExecutionTimeMilliseconds:F2}ms (exceeded threshold of {alert.Threshold:F2}ms)"); - - foreach (var context in alert.Context) - { - sb.AppendLine($"{context.Source}: {context.Elapsed:F2}ms"); - } - } - - this.Alerts.Clear(); - - this.Monitor.Log(sb.ToString(), LogLevel.Error); } + /// Begins tracking the invocation for a collection. + /// The collection name public void BeginTrackInvocation(string collectionName) { this.GetOrCreateCollectionByName(collectionName).BeginTrackInvocation(); } + /// Ends tracking the invocation for a collection. + /// public void EndTrackInvocation(string collectionName) { this.GetOrCreateCollectionByName(collectionName).EndTrackInvocation(); } - public void Track(string collectionName, string modName, Action action) + /// Tracks a single performance counter invocation in a specific collection. + /// The name of the collection. + /// The name of the source. + /// The action to execute and track invocation time for. + public void Track(string collectionName, string sourceName, Action action) { DateTime eventTime = DateTime.UtcNow; - this.Stopwatch.Reset(); - this.Stopwatch.Start(); + this.InvocationStopwatch.Reset(); + this.InvocationStopwatch.Start(); try { @@ -77,75 +67,102 @@ namespace StardewModdingAPI.Framework.PerformanceCounter } finally { - this.Stopwatch.Stop(); + this.InvocationStopwatch.Stop(); - this.GetOrCreateCollectionByName(collectionName).Track(modName, new PerformanceCounterEntry + this.GetOrCreateCollectionByName(collectionName).Track(sourceName, new PerformanceCounterEntry { EventTime = eventTime, - Elapsed = this.Stopwatch.Elapsed + ElapsedMilliseconds = this.InvocationStopwatch.Elapsed.TotalMilliseconds }); } } - public PerformanceCounterCollection GetCollectionByName(string name) + /// Gets a collection by name. + /// The name of the collection. + /// The collection or null if none was found. + private PerformanceCounterCollection GetCollectionByName(string name) { return this.PerformanceCounterCollections.FirstOrDefault(collection => collection.Name == name); } - public PerformanceCounterCollection GetOrCreateCollectionByName(string name) + /// Gets a collection by name and creates it if it doesn't exist. + /// The name of the collection. + /// The collection. + private PerformanceCounterCollection GetOrCreateCollectionByName(string name) { PerformanceCounterCollection collection = this.GetCollectionByName(name); - if (collection == null) - { - collection = new PerformanceCounterCollection(this, name); - this.PerformanceCounterCollections.Add(collection); - } + if (collection != null) return collection; + + collection = new PerformanceCounterCollection(this, name); + this.PerformanceCounterCollections.Add(collection); return collection; } - public void ResetCategory(string name) + /// Resets the performance counters for a specific collection. + /// The collection name. + public void ResetCollection(string name) { - foreach (var performanceCounterCollection in this.PerformanceCounterCollections) + foreach (PerformanceCounterCollection performanceCounterCollection in + this.PerformanceCounterCollections.Where(performanceCounterCollection => + performanceCounterCollection.Name.Equals(name, StringComparison.InvariantCultureIgnoreCase))) { - if (performanceCounterCollection.Name.Equals(name, StringComparison.InvariantCultureIgnoreCase)) - { - performanceCounterCollection.ResetCallsPerSecond(); - performanceCounterCollection.Reset(); - } + performanceCounterCollection.ResetCallsPerSecond(); + performanceCounterCollection.Reset(); } } + /// Resets performance counters for a specific source. + /// The name of the source. public void ResetSource(string name) { - foreach (var performanceCounterCollection in this.PerformanceCounterCollections) - { + foreach (PerformanceCounterCollection performanceCounterCollection in this.PerformanceCounterCollections) performanceCounterCollection.ResetSource(name); - } } + /// Print any queued alerts. + public void PrintQueuedAlerts() + { + if (this.Alerts.Count == 0) return; + + StringBuilder sb = new StringBuilder(); + + foreach (AlertEntry alert in this.Alerts) + { + sb.AppendLine($"{alert.Collection.Name} took {alert.ExecutionTimeMilliseconds:F2}ms (exceeded threshold of {alert.ThresholdMilliseconds:F2}ms)"); + foreach (AlertContext context in alert.Context.OrderByDescending(p => p.Elapsed)) + sb.AppendLine(context.ToString()); + } + + this.Alerts.Clear(); + this.Monitor.Log(sb.ToString(), LogLevel.Error); + } + + /// Adds an alert to the queue. + /// The alert to add. public void AddAlert(AlertEntry entry) { this.Alerts.Add(entry); } - public void InitializePerformanceCounterEvents(EventManager eventManager) + /// Initialized the default performance counter collections. + /// The event manager. + public void InitializePerformanceCounterCollections(EventManager eventManager) { this.PerformanceCounterCollections = new HashSet() { new EventPerformanceCounterCollection(this, eventManager.MenuChanged, false), - // Rendering Events - new EventPerformanceCounterCollection(this, eventManager.Rendering, true), + new EventPerformanceCounterCollection(this, eventManager.Rendering, false), new EventPerformanceCounterCollection(this, eventManager.Rendered, true), - new EventPerformanceCounterCollection(this, eventManager.RenderingWorld, true), + new EventPerformanceCounterCollection(this, eventManager.RenderingWorld, false), new EventPerformanceCounterCollection(this, eventManager.RenderedWorld, true), - new EventPerformanceCounterCollection(this, eventManager.RenderingActiveMenu, true), + new EventPerformanceCounterCollection(this, eventManager.RenderingActiveMenu, false), new EventPerformanceCounterCollection(this, eventManager.RenderedActiveMenu, true), - new EventPerformanceCounterCollection(this, eventManager.RenderingHud, true), + new EventPerformanceCounterCollection(this, eventManager.RenderingHud, false), new EventPerformanceCounterCollection(this, eventManager.RenderedHud, true), new EventPerformanceCounterCollection(this, eventManager.WindowResized, false), @@ -172,19 +189,19 @@ namespace StardewModdingAPI.Framework.PerformanceCounter new EventPerformanceCounterCollection(this, eventManager.CursorMoved, true), new EventPerformanceCounterCollection(this, eventManager.MouseWheelScrolled, true), - new EventPerformanceCounterCollection(this, eventManager.PeerContextReceived, true), - new EventPerformanceCounterCollection(this, eventManager.ModMessageReceived, true), - new EventPerformanceCounterCollection(this, eventManager.PeerDisconnected, true), + new EventPerformanceCounterCollection(this, eventManager.PeerContextReceived, false), + new EventPerformanceCounterCollection(this, eventManager.ModMessageReceived, false), + new EventPerformanceCounterCollection(this, eventManager.PeerDisconnected, false), new EventPerformanceCounterCollection(this, eventManager.InventoryChanged, true), - new EventPerformanceCounterCollection(this, eventManager.LevelChanged, true), - new EventPerformanceCounterCollection(this, eventManager.Warped, true), + new EventPerformanceCounterCollection(this, eventManager.LevelChanged, false), + new EventPerformanceCounterCollection(this, eventManager.Warped, false), - new EventPerformanceCounterCollection(this, eventManager.LocationListChanged, true), - new EventPerformanceCounterCollection(this, eventManager.BuildingListChanged, true), - new EventPerformanceCounterCollection(this, eventManager.LocationListChanged, true), + new EventPerformanceCounterCollection(this, eventManager.LocationListChanged, false), + new EventPerformanceCounterCollection(this, eventManager.BuildingListChanged, false), + new EventPerformanceCounterCollection(this, eventManager.LocationListChanged, false), new EventPerformanceCounterCollection(this, eventManager.DebrisListChanged, true), new EventPerformanceCounterCollection(this, eventManager.LargeTerrainFeatureListChanged, true), - new EventPerformanceCounterCollection(this, eventManager.NpcListChanged, true), + new EventPerformanceCounterCollection(this, eventManager.NpcListChanged, false), new EventPerformanceCounterCollection(this, eventManager.ObjectListChanged, true), new EventPerformanceCounterCollection(this, eventManager.ChestInventoryChanged, true), new EventPerformanceCounterCollection(this, eventManager.TerrainFeatureListChanged, true), diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index 5b0c6691..af7513e3 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -169,7 +169,7 @@ namespace StardewModdingAPI.Framework SCore.PerformanceCounterManager = new PerformanceCounterManager(this.Monitor); this.EventManager = new EventManager(this.Monitor, this.ModRegistry, SCore.PerformanceCounterManager); - SCore.PerformanceCounterManager.InitializePerformanceCounterEvents(this.EventManager); + SCore.PerformanceCounterManager.InitializePerformanceCounterCollections(this.EventManager); SCore.DeprecationManager = new DeprecationManager(this.Monitor, this.ModRegistry); diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index 8aba9b57..266e2e6f 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -312,7 +312,7 @@ namespace StardewModdingAPI.Framework try { this.DeprecationManager.PrintQueued(); - this.PerformanceCounterManager.PrintQueued(); + this.PerformanceCounterManager.PrintQueuedAlerts(); /********* ** First-tick initialization -- cgit From 1d58a525fa170a8e0de3de38477c501fb83f0b5a Mon Sep 17 00:00:00 2001 From: Drachenkaetzchen Date: Wed, 15 Jan 2020 17:42:46 +0100 Subject: Added optional right-align for the table output --- .../Framework/Commands/TrainerCommand.cs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/TrainerCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/TrainerCommand.cs index 466b8f6e..8f0d89ba 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/TrainerCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/TrainerCommand.cs @@ -66,7 +66,8 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands /// The data to display. /// The table header. /// Returns a set of fields for a data value. - protected string GetTableString(IEnumerable data, string[] header, Func getRow) + /// True to right-align the data, false for left-align. Default false. + protected string GetTableString(IEnumerable data, string[] header, Func getRow, bool rightAlign = false) { // get table data int[] widths = header.Select(p => p.Length).ToArray(); @@ -92,6 +93,15 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands }; lines.AddRange(rows); + if (rightAlign) + { + return string.Join( + Environment.NewLine, + lines.Select(line => string.Join(" | ", line.Select((field, i) => field.PadLeft(widths[i], ' ')).ToArray()) + ) + ); + } + return string.Join( Environment.NewLine, lines.Select(line => string.Join(" | ", line.Select((field, i) => field.PadRight(widths[i], ' ')).ToArray()) -- cgit From fce5814bcb150c4ff105a37dfcd57f397b117e48 Mon Sep 17 00:00:00 2001 From: Drachenkaetzchen Date: Wed, 15 Jan 2020 17:43:41 +0100 Subject: Added documentation for all commands. Renamed the "monitor" command to "trigger". Method name refactoring to be more consistent. --- .../Commands/Other/PerformanceCounterCommand.cs | 596 ++++++++++++--------- src/SMAPI/Framework/Events/EventManager.cs | 1 + src/SMAPI/Framework/Events/ManagedEvent.cs | 1 + .../PerformanceCounter/PerformanceCounter.cs | 4 + .../PerformanceCounterManager.cs | 6 +- 5 files changed, 361 insertions(+), 247 deletions(-) (limited to 'src') diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/PerformanceCounterCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/PerformanceCounterCommand.cs index 750e3792..82b44562 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/PerformanceCounterCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/PerformanceCounterCommand.cs @@ -7,65 +7,71 @@ using StardewModdingAPI.Framework.PerformanceCounter; namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other { + // ReSharper disable once UnusedType.Global internal class PerformanceCounterCommand : TrainerCommand { - private readonly Dictionary CommandNames = new Dictionary() + /// The command names and aliases + private readonly Dictionary SubCommandNames = new Dictionary() { - {Command.Summary, new[] {"summary", "sum", "s"}}, - {Command.Detail, new[] {"detail", "d"}}, - {Command.Reset, new[] {"reset", "r"}}, - {Command.Monitor, new[] {"monitor"}}, - {Command.Examples, new[] {"examples"}}, - {Command.Concepts, new[] {"concepts"}}, - {Command.Help, new[] {"help"}}, + {SubCommand.Summary, new[] {"summary", "sum", "s"}}, + {SubCommand.Detail, new[] {"detail", "d"}}, + {SubCommand.Reset, new[] {"reset", "r"}}, + {SubCommand.Trigger, new[] {"trigger"}}, + {SubCommand.Examples, new[] {"examples"}}, + {SubCommand.Concepts, new[] {"concepts"}}, + {SubCommand.Help, new[] {"help"}}, }; - private enum Command + /// The available commands enum + private enum SubCommand { Summary, Detail, Reset, - Monitor, + Trigger, Examples, Help, Concepts, None } + /// Construct an instance. public PerformanceCounterCommand() : base("pc", PerformanceCounterCommand.GetDescription()) { } + /// Handle the command. + /// Writes messages to the console and log file. + /// The command name. + /// The command arguments. public override void Handle(IMonitor monitor, string command, ArgumentParser args) { if (args.TryGet(0, "command", out string subCommandString, false)) { - Command subCommand = this.ParseCommandString(subCommandString); + SubCommand subSubCommand = this.ParseCommandString(subCommandString); - switch (subCommand) + switch (subSubCommand) { - case Command.Summary: - this.DisplayPerformanceCounterSummary(monitor, args); + case SubCommand.Summary: + this.HandleSummarySubCommand(monitor, args); break; - case Command.Detail: - this.DisplayPerformanceCounterDetail(monitor, args); + case SubCommand.Detail: + this.HandleDetailSubCommand(monitor, args); break; - case Command.Reset: - this.ResetCounter(monitor, args); + case SubCommand.Reset: + this.HandleResetSubCommand(monitor, args); break; - case Command.Monitor: - this.HandleMonitor(monitor, args); + case SubCommand.Trigger: + this.HandleTriggerSubCommand(monitor, args); break; - case Command.Examples: + case SubCommand.Examples: break; - case Command.Concepts: - this.ShowHelp(monitor, Command.Concepts); + case SubCommand.Concepts: + this.OutputHelp(monitor, SubCommand.Concepts); break; - case Command.Help: - args.TryGet(1, "command", out string commandString, true); - - var helpCommand = this.ParseCommandString(commandString); - this.ShowHelp(monitor, helpCommand); + case SubCommand.Help: + if (args.TryGet(1, "command", out string commandString)) + this.OutputHelp(monitor, this.ParseCommandString(commandString)); break; default: this.LogUsageError(monitor, $"Unknown command {subCommandString}"); @@ -73,60 +79,154 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other } } else + this.HandleSummarySubCommand(monitor, args); + } + + /// Handles the summary sub command. + /// Writes messages to the console and log file. + /// The command arguments. + private void HandleSummarySubCommand(IMonitor monitor, ArgumentParser args) + { + IEnumerable data; + + if (!args.TryGet(1, "mode", out string mode, false)) + { + mode = "important"; + } + + switch (mode) + { + case null: + case "important": + data = SCore.PerformanceCounterManager.PerformanceCounterCollections.Where(p => p.IsImportant); + break; + case "all": + data = SCore.PerformanceCounterManager.PerformanceCounterCollections; + break; + default: + data = SCore.PerformanceCounterManager.PerformanceCounterCollections.Where(p => + p.Name.ToLowerInvariant().Contains(mode.ToLowerInvariant())); + break; + } + + double? threshold = null; + + if (args.TryGetDecimal(2, "threshold", out decimal t, false)) { - this.DisplayPerformanceCounterSummary(monitor, args); + threshold = (double?) t; } + + StringBuilder sb = new StringBuilder(); + + sb.AppendLine("Summary:"); + sb.AppendLine(this.GetTableString( + data: data, + header: new[] {"Collection", "Avg Calls/s", "Avg Execution Time (Game)", "Avg Execution Time (Mods)", "Avg Execution Time (Game+Mods)"}, + getRow: item => new[] + { + item.Name, + item.GetAverageCallsPerSecond().ToString(), + this.FormatMilliseconds(item.GetGameAverageExecutionTime(), threshold), + this.FormatMilliseconds(item.GetModsAverageExecutionTime(), threshold), + this.FormatMilliseconds(item.GetAverageExecutionTime(), threshold) + }, + true + )); + + monitor.Log(sb.ToString(), LogLevel.Info); } - private Command ParseCommandString(string command) + /// Handles the detail sub command. + /// Writes messages to the console and log file. + /// The command arguments. + private void HandleDetailSubCommand(IMonitor monitor, ArgumentParser args) { - foreach (var i in this.CommandNames.Where(i => i.Value.Any(str => str.Equals(command, StringComparison.InvariantCultureIgnoreCase)))) + var collections = new List(); + TimeSpan averageInterval = TimeSpan.FromSeconds(60); + double? thresholdMilliseconds = null; + string sourceFilter = null; + + if (args.TryGet(1, "collection", out string collectionName)) { - return i.Key; + collections.AddRange(SCore.PerformanceCounterManager.PerformanceCounterCollections.Where( + collection => collection.Name.ToLowerInvariant().Contains(collectionName.ToLowerInvariant()))); + + if (args.IsDecimal(2) && args.TryGetDecimal(2, "threshold", out decimal value, false)) + { + thresholdMilliseconds = (double?) value; + } + else + { + if (args.TryGet(2, "source", out string sourceName, false)) + { + sourceFilter = sourceName; + } + } } - return Command.None; + foreach (PerformanceCounterCollection c in collections) + { + this.OutputPerformanceCollectionDetail(monitor, c, averageInterval, thresholdMilliseconds, sourceFilter); + } } - private void HandleMonitor(IMonitor monitor, ArgumentParser args) + /// Handles the trigger sub command. + /// Writes messages to the console and log file. + /// The command arguments. + private void HandleTriggerSubCommand(IMonitor monitor, ArgumentParser args) { if (args.TryGet(1, "mode", out string mode, false)) { switch (mode) { case "list": - this.ListMonitors(monitor); + this.OutputAlertTriggers(monitor); break; case "collection": - args.TryGet(2, "name", out string collectionName); - decimal threshold = 0; - if (args.IsDecimal(3) && args.TryGetDecimal(3, "threshold", out threshold, false)) + if (args.TryGet(2, "name", out string collectionName)) { - this.SetCollectionMonitor(monitor, collectionName, null, (double)threshold); - } else if (args.TryGet(3, "source", out string source)) - { - if (args.TryGetDecimal(4, "threshold", out threshold)) + if (args.TryGetDecimal(3, "threshold", out decimal threshold)) { - this.SetCollectionMonitor(monitor, collectionName, source, (double) threshold); + if (args.TryGet(4, "source", out string source, false)) + { + this.ConfigureAlertTrigger(monitor, collectionName, source, threshold); + } + else + { + this.ConfigureAlertTrigger(monitor, collectionName, null, threshold); + } } } break; + case "pause": + SCore.PerformanceCounterManager.PauseAlerts = true; + monitor.Log($"Alerts are now paused.", LogLevel.Info); + break; + case "resume": + SCore.PerformanceCounterManager.PauseAlerts = false; + monitor.Log($"Alerts are now resumed.", LogLevel.Info); + break; case "clear": - this.ClearMonitors(monitor); + this.ClearAlertTriggers(monitor); break; default: - monitor.Log($"Unknown mode {mode}. See 'pc help monitor' for usage."); + this.LogUsageError(monitor, $"Unknown mode {mode}. See 'pc help trigger' for usage."); break; } } else { - this.ListMonitors(monitor); + this.OutputAlertTriggers(monitor); } } - private void SetCollectionMonitor(IMonitor monitor, string collectionName, string sourceName, double threshold) + /// Sets up an an alert trigger. + /// Writes messages to the console and log file. + /// The name of the collection. + /// The name of the source, or null for all sources. + /// The trigger threshold, or 0 to remove. + private void ConfigureAlertTrigger(IMonitor monitor, string collectionName, string sourceName, decimal threshold) { foreach (PerformanceCounterCollection collection in SCore.PerformanceCounterManager.PerformanceCounterCollections) { @@ -134,9 +234,18 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other { if (sourceName == null) { - collection.EnableAlerts = true; - collection.AlertThresholdMilliseconds = threshold; - monitor.Log($"Set up monitor for '{collectionName}' with '{this.FormatMilliseconds(threshold)}'", LogLevel.Info); + if (threshold != 0) + { + collection.EnableAlerts = true; + collection.AlertThresholdMilliseconds = (double) threshold; + monitor.Log($"Set up alert triggering for '{collectionName}' with '{this.FormatMilliseconds((double?) threshold)}'", LogLevel.Info); + } + else + { + collection.EnableAlerts = false; + monitor.Log($"Cleared alert triggering for '{collection}'."); + } + return; } else @@ -145,9 +254,17 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other { if (performanceCounter.Value.Source.ToLowerInvariant().Equals(sourceName.ToLowerInvariant())) { - performanceCounter.Value.EnableAlerts = true; - performanceCounter.Value.AlertThresholdMilliseconds = threshold; - monitor.Log($"Set up monitor for '{sourceName}' in collection '{collectionName}' with '{this.FormatMilliseconds(threshold)}", LogLevel.Info); + if (threshold != 0) + { + performanceCounter.Value.EnableAlerts = true; + performanceCounter.Value.AlertThresholdMilliseconds = (double) threshold; + monitor.Log($"Set up alert triggering for '{sourceName}' in collection '{collectionName}' with '{this.FormatMilliseconds((double?) threshold)}", LogLevel.Info); + } + else + { + performanceCounter.Value.EnableAlerts = false; + } + return; } } @@ -162,15 +279,17 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other } - private void ClearMonitors(IMonitor monitor) + /// Clears alert triggering for all collections. + /// Writes messages to the console and log file. + private void ClearAlertTriggers(IMonitor monitor) { - int clearedCounters = 0; + int clearedTriggers = 0; foreach (PerformanceCounterCollection collection in SCore.PerformanceCounterManager.PerformanceCounterCollections) { if (collection.EnableAlerts) { collection.EnableAlerts = false; - clearedCounters++; + clearedTriggers++; } foreach (var performanceCounter in collection.PerformanceCounters) @@ -178,82 +297,203 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other if (performanceCounter.Value.EnableAlerts) { performanceCounter.Value.EnableAlerts = false; - clearedCounters++; + clearedTriggers++; } } } - monitor.Log($"Cleared {clearedCounters} counters.", LogLevel.Info); + monitor.Log($"Cleared {clearedTriggers} alert triggers.", LogLevel.Info); } - private void ListMonitors(IMonitor monitor) + /// Lists all configured alert triggers. + /// Writes messages to the console and log file. + private void OutputAlertTriggers(IMonitor monitor) { StringBuilder sb = new StringBuilder(); + sb.AppendLine("Configured triggers:"); sb.AppendLine(); - sb.AppendLine(); - var collectionMonitors = new List<(string collectionName, double threshold)>(); - var sourceMonitors = new List<(string collectionName, string sourceName, double threshold)>(); + var collectionTriggers = new List<(string collectionName, double threshold)>(); + var sourceTriggers = new List<(string collectionName, string sourceName, double threshold)>(); foreach (PerformanceCounterCollection collection in SCore.PerformanceCounterManager.PerformanceCounterCollections) { if (collection.EnableAlerts) { - collectionMonitors.Add((collection.Name, collection.AlertThresholdMilliseconds)); + collectionTriggers.Add((collection.Name, collection.AlertThresholdMilliseconds)); } - sourceMonitors.AddRange(from performanceCounter in + sourceTriggers.AddRange(from performanceCounter in collection.PerformanceCounters where performanceCounter.Value.EnableAlerts - select (collection.Name, performanceCounter.Value.Source, MonitorThresholdMilliseconds: performanceCounter.Value.AlertThresholdMilliseconds)); + select (collection.Name, performanceCounter.Value.Source, performanceCounter.Value.AlertThresholdMilliseconds)); } - if (collectionMonitors.Count > 0) + if (collectionTriggers.Count > 0) { - sb.AppendLine("Collection Monitors:"); + sb.AppendLine("Collection Triggers:"); sb.AppendLine(); sb.AppendLine(this.GetTableString( - data: collectionMonitors, + data: collectionTriggers, header: new[] {"Collection", "Threshold"}, getRow: item => new[] { item.collectionName, this.FormatMilliseconds(item.threshold) - } + }, + true )); sb.AppendLine(); - - + } + else + { + sb.AppendLine("No collection triggers."); } - if (sourceMonitors.Count > 0) + if (sourceTriggers.Count > 0) { - sb.AppendLine("Source Monitors:"); + sb.AppendLine("Source Triggers:"); sb.AppendLine(); sb.AppendLine(this.GetTableString( - data: sourceMonitors, + data: sourceTriggers, header: new[] {"Collection", "Source", "Threshold"}, getRow: item => new[] { item.collectionName, item.sourceName, this.FormatMilliseconds(item.threshold) - } + }, + true )); sb.AppendLine(); } + else + { + sb.AppendLine("No source triggers."); + } monitor.Log(sb.ToString(), LogLevel.Info); } - private void ShowHelp(IMonitor monitor, Command command) + /// Handles the reset sub command. + /// Writes messages to the console and log file. + /// The command arguments. + private void HandleResetSubCommand(IMonitor monitor, ArgumentParser args) + { + if (args.TryGet(1, "type", out string type, false, new []{"category", "source"})) + { + args.TryGet(2, "name", out string name); + + switch (type) + { + case "category": + SCore.PerformanceCounterManager.ResetCollection(name); + monitor.Log($"All performance counters for category {name} are now cleared.", LogLevel.Info); + break; + case "source": + SCore.PerformanceCounterManager.ResetSource(name); + monitor.Log($"All performance counters for source {name} are now cleared.", LogLevel.Info); + break; + } + } + else + { + SCore.PerformanceCounterManager.Reset(); + monitor.Log("All performance counters are now cleared.", LogLevel.Info); + } + } + + + /// Outputs the details for a collection. + /// Writes messages to the console and log file. + /// The collection. + /// The interval over which to calculate the averages. + /// The threshold. + /// The source filter. + private void OutputPerformanceCollectionDetail(IMonitor monitor, PerformanceCounterCollection collection, + TimeSpan averageInterval, double? thresholdMilliseconds, string sourceFilter = null) + { + StringBuilder sb = new StringBuilder($"Performance Counter for {collection.Name}:\n\n"); + + List> data = collection.PerformanceCounters.ToList(); + + if (sourceFilter != null) + { + data = collection.PerformanceCounters.Where(p => + p.Value.Source.ToLowerInvariant().Contains(sourceFilter.ToLowerInvariant())).ToList(); + } + + if (thresholdMilliseconds != null) + { + data = data.Where(p => p.Value.GetAverage(averageInterval) >= thresholdMilliseconds).ToList(); + } + + if (data.Any()) + { + sb.AppendLine(this.GetTableString( + data: data, + header: new[] {"Mod", $"Avg Execution Time (last {(int) averageInterval.TotalSeconds}s)", "Last Execution Time", "Peak Execution Time"}, + getRow: item => new[] + { + item.Key, + this.FormatMilliseconds(item.Value.GetAverage(averageInterval), thresholdMilliseconds), + this.FormatMilliseconds(item.Value.GetLastEntry()?.ElapsedMilliseconds), + this.FormatMilliseconds(item.Value.GetPeak()?.ElapsedMilliseconds) + }, + true + )); + } + else + { + sb.Clear(); + sb.AppendLine($"Performance Counter for {collection.Name}: none."); + } + + monitor.Log(sb.ToString(), LogLevel.Info); + } + + /// Parses a command string and returns the associated command. + /// The command string + /// The parsed command. + private SubCommand ParseCommandString(string commandString) + { + foreach (var i in this.SubCommandNames.Where(i => + i.Value.Any(str => str.Equals(commandString, StringComparison.InvariantCultureIgnoreCase)))) + { + return i.Key; + } + + return SubCommand.None; + } + + + /// Formats the given milliseconds value into a string format. Optionally + /// allows a threshold to return "-" if the value is less than the threshold. + /// The milliseconds to format. Returns "-" if null + /// The threshold. Any value below this is returned as "-". + /// The formatted milliseconds. + private string FormatMilliseconds(double? milliseconds, double? thresholdMilliseconds = null) + { + if (milliseconds == null || (thresholdMilliseconds != null && milliseconds < thresholdMilliseconds)) + { + return "-"; + } + + return ((double) milliseconds).ToString("F2"); + } + + /// Shows detailed help for a specific sub command. + /// The output monitor + /// The sub command + private void OutputHelp(IMonitor monitor, SubCommand subCommand) { StringBuilder sb = new StringBuilder(); sb.AppendLine(); - switch (command) + + switch (subCommand) { - case Command.Concepts: + case SubCommand.Concepts: sb.AppendLine("A performance counter is a metric which measures execution time. Each performance"); sb.AppendLine("counter consists of:"); sb.AppendLine(); @@ -271,7 +511,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other sb.AppendLine(); sb.AppendLine("[1] https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Events"); break; - case Command.Detail: + case SubCommand.Detail: sb.AppendLine("Usage: pc detail "); sb.AppendLine(" pc detail "); sb.AppendLine(); @@ -288,61 +528,66 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other sb.AppendLine("pc detail Display.Rendering Pathoschild.ChestsAnywhere Displays the 'Display.Rendering' performance counter for 'Pathoschild.ChestsAnywhere'"); sb.AppendLine("pc detail Display.Rendering 5 Displays the 'Display.Rendering' performance counters exceeding an average of 5ms"); break; - case Command.Summary: - sb.AppendLine("Usage: pc summary "); + case SubCommand.Summary: + sb.AppendLine("Usage: pc summary "); sb.AppendLine(); sb.AppendLine("Displays the performance counter summary."); sb.AppendLine(); sb.AppendLine("Arguments:"); - sb.AppendLine(" Optional. Defaults to 'important' if omitted. Specifies one of these modes:"); - sb.AppendLine(" - all Displays performance counters from all collections"); - sb.AppendLine(" - important Displays only important performance counter collections"); + sb.AppendLine(" Optional. Defaults to 'important' if omitted. Specifies one of these modes:"); + sb.AppendLine(" - all Displays performance counters from all collections"); + sb.AppendLine(" - important Displays only important performance counter collections"); sb.AppendLine(); - sb.AppendLine(" Optional. Only shows performance counter collections matching the given name"); + sb.AppendLine(" Optional. Only shows performance counter collections matching the given name"); + sb.AppendLine(" Optional. Hides the actual execution time if it is below this threshold"); sb.AppendLine(); sb.AppendLine("Examples:"); sb.AppendLine("pc summary all Shows all events"); + sb.AppendLine("pc summary all 5 Shows all events"); sb.AppendLine("pc summary Display.Rendering Shows only the 'Display.Rendering' collection"); break; - case Command.Monitor: - sb.AppendLine("Usage: pc monitor "); - sb.AppendLine("Usage: pc monitor "); + case SubCommand.Trigger: + sb.AppendLine("Usage: pc trigger "); + sb.AppendLine("Usage: pc trigger collection "); + sb.AppendLine("Usage: pc trigger collection "); sb.AppendLine(); - sb.AppendLine("Manages monitoring settings."); + sb.AppendLine("Manages alert triggers."); sb.AppendLine(); sb.AppendLine("Arguments:"); - sb.AppendLine(" Optional. Specifies if a specific source or a specific collection should be monitored."); - sb.AppendLine(" - list Lists current monitoring settings"); - sb.AppendLine(" - collection Sets up a monitor for a collection"); - sb.AppendLine(" - clear Clears all monitoring entries"); + sb.AppendLine(" Optional. Specifies if a specific source or a specific collection should be triggered."); + sb.AppendLine(" - list Lists current triggers"); + sb.AppendLine(" - collection Sets up a trigger for a collection"); + sb.AppendLine(" - clear Clears all trigger entries"); + sb.AppendLine(" - pause Pauses triggering of alerts"); + sb.AppendLine(" - resume Resumes triggering of alerts"); sb.AppendLine(" Defaults to 'list' if not specified."); sb.AppendLine(); sb.AppendLine(" Required if the mode 'collection' is specified."); - sb.AppendLine(" Specifies the name of the collection to be monitored. Must be an exact match."); + sb.AppendLine(" Specifies the name of the collection to be triggered. Must be an exact match."); sb.AppendLine(); sb.AppendLine(" Optional. Specifies the name of a specific source. Must be an exact match."); sb.AppendLine(); sb.AppendLine(" Required if the mode 'collection' is specified."); sb.AppendLine(" Specifies the threshold in milliseconds (fractions allowed)."); - sb.AppendLine(" Can also be 'remove' to remove the threshold."); + sb.AppendLine(" Specify '0' to remove the threshold."); sb.AppendLine(); sb.AppendLine("Examples:"); sb.AppendLine(); - sb.AppendLine("pc monitor collection Display.Rendering 10"); - sb.AppendLine(" Sets up monitoring to write an alert on the console if the execution time of all performance counters in"); + sb.AppendLine("pc trigger collection Display.Rendering 10"); + sb.AppendLine(" Sets up an alert trigger which writes on the console if the execution time of all performance counters in"); sb.AppendLine(" the 'Display.Rendering' collection exceed 10 milliseconds."); sb.AppendLine(); - sb.AppendLine("pc monitor collection Display.Rendering Pathoschild.ChestsAnywhere 5"); - sb.AppendLine(" Sets up monitoring to write an alert on the console if the execution time of Pathoschild.ChestsAnywhere in"); + sb.AppendLine("pc trigger collection Display.Rendering 5 Pathoschild.ChestsAnywhere"); + sb.AppendLine(" Sets up an alert trigger to write on the console if the execution time of Pathoschild.ChestsAnywhere in"); sb.AppendLine(" the 'Display.Rendering' collection exceed 5 milliseconds."); sb.AppendLine(); - sb.AppendLine("pc monitor collection Display.Rendering remove"); + sb.AppendLine("pc trigger collection Display.Rendering 0"); sb.AppendLine(" Removes the threshold previously defined from the collection. Note that source-specific thresholds are left intact."); sb.AppendLine(); - sb.AppendLine("pc monitor clear"); - sb.AppendLine(" Clears all previously setup monitors."); + sb.AppendLine("pc trigger clear"); + sb.AppendLine(" Clears all previously setup alert triggers."); break; - case Command.Reset: + case SubCommand.Reset: sb.AppendLine("Usage: pc reset "); sb.AppendLine(); sb.AppendLine("Resets performance counters."); @@ -368,153 +613,12 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other monitor.Log(sb.ToString(), LogLevel.Info); } - private void ResetCounter(IMonitor monitor, ArgumentParser args) - { - if (args.TryGet(1, "type", out string type, false)) - { - args.TryGet(2, "name", out string name); - - switch (type) - { - case "category": - SCore.PerformanceCounterManager.ResetCollection(name); - monitor.Log($"All performance counters for category {name} are now cleared.", LogLevel.Info); - break; - case "mod": - SCore.PerformanceCounterManager.ResetSource(name); - monitor.Log($"All performance counters for mod {name} are now cleared.", LogLevel.Info); - break; - } - } - else - { - SCore.PerformanceCounterManager.Reset(); - monitor.Log("All performance counters are now cleared.", LogLevel.Info); - } - } - - private void DisplayPerformanceCounterSummary(IMonitor monitor, ArgumentParser args) - { - IEnumerable data; - - if (!args.TryGet(1, "mode", out string mode, false)) - { - mode = "important"; - } - - switch (mode) - { - case null: - case "important": - data = SCore.PerformanceCounterManager.PerformanceCounterCollections.Where(p => p.IsImportant); - break; - case "all": - data = SCore.PerformanceCounterManager.PerformanceCounterCollections; - break; - default: - data = SCore.PerformanceCounterManager.PerformanceCounterCollections.Where(p => - p.Name.ToLowerInvariant().Contains(mode.ToLowerInvariant())); - break; - } - - StringBuilder sb = new StringBuilder(); - - sb.AppendLine("Summary:"); - sb.AppendLine(this.GetTableString( - data: data, - header: new[] {"Collection", "Avg Calls/s", "Avg Execution Time (Game)", "Avg Execution Time (Mods)", "Avg Execution Time (Game+Mods)"}, - getRow: item => new[] - { - item.Name, - item.GetAverageCallsPerSecond().ToString(), - this.FormatMilliseconds(item.GetGameAverageExecutionTime()), - this.FormatMilliseconds(item.GetModsAverageExecutionTime()), - this.FormatMilliseconds(item.GetAverageExecutionTime()) - } - )); - - monitor.Log(sb.ToString(), LogLevel.Info); - } - - private void DisplayPerformanceCounterDetail(IMonitor monitor, ArgumentParser args) - { - List collections = new List(); - TimeSpan averageInterval = TimeSpan.FromSeconds(60); - double? thresholdMilliseconds = null; - string sourceFilter = null; - - if (args.TryGet(1, "collection", out string collectionName)) - { - collections.AddRange(SCore.PerformanceCounterManager.PerformanceCounterCollections.Where(collection => collection.Name.ToLowerInvariant().Contains(collectionName.ToLowerInvariant()))); - - if (args.IsDecimal(2) && args.TryGetDecimal(2, "threshold", out decimal value, false)) - { - thresholdMilliseconds = (double?) value; - } - else - { - if (args.TryGet(2, "source", out string sourceName, false)) - { - sourceFilter = sourceName; - } - } - } - - foreach (var c in collections) - { - this.DisplayPerformanceCollectionDetail(monitor, c, averageInterval, thresholdMilliseconds, sourceFilter); - } - } - - private void DisplayPerformanceCollectionDetail(IMonitor monitor, PerformanceCounterCollection collection, - TimeSpan averageInterval, double? thresholdMilliseconds, string sourceFilter = null) - { - StringBuilder sb = new StringBuilder($"Performance Counter for {collection.Name}:\n\n"); - - IEnumerable> data = collection.PerformanceCounters; - - if (sourceFilter != null) - { - data = collection.PerformanceCounters.Where(p => - p.Value.Source.ToLowerInvariant().Contains(sourceFilter.ToLowerInvariant())); - } - - if (thresholdMilliseconds != null) - { - data = data.Where(p => p.Value.GetAverage(averageInterval) >= thresholdMilliseconds); - } - - sb.AppendLine(this.GetTableString( - data: data, - header: new[] {"Mod", $"Avg Execution Time (last {(int) averageInterval.TotalSeconds}s)", "Last Execution Time", "Peak Execution Time"}, - getRow: item => new[] - { - item.Key, - this.FormatMilliseconds(item.Value.GetAverage(averageInterval), thresholdMilliseconds), - this.FormatMilliseconds(item.Value.GetLastEntry()?.ElapsedMilliseconds), - this.FormatMilliseconds(item.Value.GetPeak()?.ElapsedMilliseconds) - } - )); - - monitor.Log(sb.ToString(), LogLevel.Info); - } - - private string FormatMilliseconds(double? milliseconds, double? thresholdMilliseconds = null) - { - if (milliseconds == null || (thresholdMilliseconds != null && milliseconds < thresholdMilliseconds)) - { - return "-"; - } - - return ((double) milliseconds).ToString("F2"); - } - /// Get the command description. private static string GetDescription() { StringBuilder sb = new StringBuilder(); - sb.AppendLine("Displays and configured performance counters."); + sb.AppendLine("Displays and configures performance counters."); sb.AppendLine(); sb.AppendLine("A performance counter records the invocation time of in-game events being"); sb.AppendLine("processed by mods or the game itself. See 'concepts' for a detailed explanation."); @@ -526,7 +630,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other sb.AppendLine(" summary|sum|s Displays a summary of important or all collections"); sb.AppendLine(" detail|d Shows performance counter information for a given collection"); sb.AppendLine(" reset|r Resets the performance counters"); - sb.AppendLine(" monitor Configures monitoring settings"); + sb.AppendLine(" trigger Configures alert triggers"); sb.AppendLine(" examples Displays various examples"); sb.AppendLine(" concepts Displays an explanation of the performance counter concepts"); sb.AppendLine(" help Displays verbose help for the available commands"); diff --git a/src/SMAPI/Framework/Events/EventManager.cs b/src/SMAPI/Framework/Events/EventManager.cs index 9c65a6cc..19a4dff8 100644 --- a/src/SMAPI/Framework/Events/EventManager.cs +++ b/src/SMAPI/Framework/Events/EventManager.cs @@ -174,6 +174,7 @@ namespace StardewModdingAPI.Framework.Events /// Construct an instance. /// Writes messages to the log. /// The mod registry with which to identify mods. + /// The performance counter manager. public EventManager(IMonitor monitor, ModRegistry modRegistry, PerformanceCounterManager performanceCounterManager) { // create shortcut initializers diff --git a/src/SMAPI/Framework/Events/ManagedEvent.cs b/src/SMAPI/Framework/Events/ManagedEvent.cs index bba94c35..dfdd7449 100644 --- a/src/SMAPI/Framework/Events/ManagedEvent.cs +++ b/src/SMAPI/Framework/Events/ManagedEvent.cs @@ -40,6 +40,7 @@ namespace StardewModdingAPI.Framework.Events /// A human-readable name for the event. /// Writes messages to the log. /// The mod registry with which to identify mods. + /// The performance counter manager public ManagedEvent(string eventName, IMonitor monitor, ModRegistry modRegistry, PerformanceCounterManager performanceCounterManager) { this.EventName = eventName; diff --git a/src/SMAPI/Framework/PerformanceCounter/PerformanceCounter.cs b/src/SMAPI/Framework/PerformanceCounter/PerformanceCounter.cs index b2ec4c90..33ddde2f 100644 --- a/src/SMAPI/Framework/PerformanceCounter/PerformanceCounter.cs +++ b/src/SMAPI/Framework/PerformanceCounter/PerformanceCounter.cs @@ -114,6 +114,10 @@ namespace StardewModdingAPI.Framework.PerformanceCounter DateTime start = relativeTo.Value.Subtract(range); var entries = this._counter.Where(x => (x.EventTime >= start) && (x.EventTime <= relativeTo)); + + if (!entries.Any()) + return 0; + return entries.Average(x => x.ElapsedMilliseconds); } } diff --git a/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterManager.cs b/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterManager.cs index d8f1f172..49720431 100644 --- a/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterManager.cs +++ b/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterManager.cs @@ -20,6 +20,9 @@ namespace StardewModdingAPI.Framework.PerformanceCounter /// The invocation stopwatch. private readonly Stopwatch InvocationStopwatch = new Stopwatch(); + /// Specifies if alerts should be paused. + public bool PauseAlerts { get; set; } + /// Constructs a performance counter manager. /// The monitor for output logging. public PerformanceCounterManager(IMonitor monitor) @@ -144,7 +147,8 @@ namespace StardewModdingAPI.Framework.PerformanceCounter /// The alert to add. public void AddAlert(AlertEntry entry) { - this.Alerts.Add(entry); + if (!this.PauseAlerts) + this.Alerts.Add(entry); } /// Initialized the default performance counter collections. -- cgit From 238b5db4f7f2f05e8967cc5eda761733d4bf35b4 Mon Sep 17 00:00:00 2001 From: Drachenkaetzchen Date: Wed, 15 Jan 2020 17:50:12 +0100 Subject: Added "trigger dump" command to dump the configured triggers as commands for copy'n'paste --- .../Commands/Other/PerformanceCounterCommand.cs | 67 +++++++++++++++------- 1 file changed, 47 insertions(+), 20 deletions(-) (limited to 'src') diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/PerformanceCounterCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/PerformanceCounterCommand.cs index 82b44562..f096614f 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/PerformanceCounterCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/PerformanceCounterCommand.cs @@ -206,6 +206,9 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other SCore.PerformanceCounterManager.PauseAlerts = false; monitor.Log($"Alerts are now resumed.", LogLevel.Info); break; + case "dump": + this.OutputAlertTriggers(monitor, true); + break; case "clear": this.ClearAlertTriggers(monitor); break; @@ -308,7 +311,8 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other /// Lists all configured alert triggers. /// Writes messages to the console and log file. - private void OutputAlertTriggers(IMonitor monitor) + /// True to dump the triggers as commands. + private void OutputAlertTriggers(IMonitor monitor, bool asDump = false) { StringBuilder sb = new StringBuilder(); sb.AppendLine("Configured triggers:"); @@ -332,16 +336,27 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other { sb.AppendLine("Collection Triggers:"); sb.AppendLine(); - sb.AppendLine(this.GetTableString( - data: collectionTriggers, - header: new[] {"Collection", "Threshold"}, - getRow: item => new[] + + if (asDump) + { + foreach (var item in collectionTriggers) { - item.collectionName, - this.FormatMilliseconds(item.threshold) - }, - true - )); + sb.AppendLine($"pc trigger {item.collectionName} {item.threshold}"); + } + } + else + { + sb.AppendLine(this.GetTableString( + data: collectionTriggers, + header: new[] {"Collection", "Threshold"}, + getRow: item => new[] + { + item.collectionName, + this.FormatMilliseconds(item.threshold) + }, + true + )); + } sb.AppendLine(); } @@ -354,17 +369,28 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other { sb.AppendLine("Source Triggers:"); sb.AppendLine(); - sb.AppendLine(this.GetTableString( - data: sourceTriggers, - header: new[] {"Collection", "Source", "Threshold"}, - getRow: item => new[] + + if (asDump) + { + foreach (var item in sourceTriggers) { - item.collectionName, - item.sourceName, - this.FormatMilliseconds(item.threshold) - }, - true - )); + sb.AppendLine($"pc trigger {item.collectionName} {item.threshold} {item.sourceName}"); + } + } + else + { + sb.AppendLine(this.GetTableString( + data: sourceTriggers, + header: new[] {"Collection", "Source", "Threshold"}, + getRow: item => new[] + { + item.collectionName, + item.sourceName, + this.FormatMilliseconds(item.threshold) + }, + true + )); + } sb.AppendLine(); } @@ -560,6 +586,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other sb.AppendLine(" - clear Clears all trigger entries"); sb.AppendLine(" - pause Pauses triggering of alerts"); sb.AppendLine(" - resume Resumes triggering of alerts"); + sb.AppendLine(" - dump Dumps all triggers as commands for copy and paste"); sb.AppendLine(" Defaults to 'list' if not specified."); sb.AppendLine(); sb.AppendLine(" Required if the mode 'collection' is specified."); -- cgit From 84973ce5727ad20fe8b8ba4f89e59c8b754f799e Mon Sep 17 00:00:00 2001 From: Drachenkaetzchen Date: Wed, 15 Jan 2020 19:08:50 +0100 Subject: Added peak execution time over the last 60 seconds --- .../Commands/Other/PerformanceCounterCommand.cs | 12 ++++-- .../Framework/PerformanceCounter/PeakEntry.cs | 24 +++++++++++ .../PerformanceCounter/PerformanceCounter.cs | 20 +++++++++ .../PerformanceCounterCollection.cs | 47 ++++++++++++++++++---- .../PerformanceCounterManager.cs | 5 +++ 5 files changed, 96 insertions(+), 12 deletions(-) create mode 100644 src/SMAPI/Framework/PerformanceCounter/PeakEntry.cs (limited to 'src') diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/PerformanceCounterCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/PerformanceCounterCommand.cs index f096614f..d49fc537 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/PerformanceCounterCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/PerformanceCounterCommand.cs @@ -118,17 +118,20 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other StringBuilder sb = new StringBuilder(); + TimeSpan peakSpan = TimeSpan.FromSeconds(60); + sb.AppendLine("Summary:"); sb.AppendLine(this.GetTableString( data: data, - header: new[] {"Collection", "Avg Calls/s", "Avg Execution Time (Game)", "Avg Execution Time (Mods)", "Avg Execution Time (Game+Mods)"}, + header: new[] {"Collection", "Avg Calls/s", "Avg Exec Time (Game)", "Avg Exec Time (Mods)", "Avg Exec Time (Game+Mods)", "Peak Exec Time (60s)"}, getRow: item => new[] { item.Name, item.GetAverageCallsPerSecond().ToString(), this.FormatMilliseconds(item.GetGameAverageExecutionTime(), threshold), this.FormatMilliseconds(item.GetModsAverageExecutionTime(), threshold), - this.FormatMilliseconds(item.GetAverageExecutionTime(), threshold) + this.FormatMilliseconds(item.GetAverageExecutionTime(), threshold), + this.FormatMilliseconds(item.GetPeakExecutionTime(peakSpan), threshold) }, true )); @@ -459,13 +462,14 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other { sb.AppendLine(this.GetTableString( data: data, - header: new[] {"Mod", $"Avg Execution Time (last {(int) averageInterval.TotalSeconds}s)", "Last Execution Time", "Peak Execution Time"}, + header: new[] {"Mod", $"Avg Exec Time (last {(int) averageInterval.TotalSeconds}s)", "Last Exec Time", "Peak Exec Time", $"Peak Exec Time (last {(int) averageInterval.TotalSeconds}s)"}, getRow: item => new[] { item.Key, this.FormatMilliseconds(item.Value.GetAverage(averageInterval), thresholdMilliseconds), this.FormatMilliseconds(item.Value.GetLastEntry()?.ElapsedMilliseconds), - this.FormatMilliseconds(item.Value.GetPeak()?.ElapsedMilliseconds) + this.FormatMilliseconds(item.Value.GetPeak()?.ElapsedMilliseconds), + this.FormatMilliseconds(item.Value.GetPeak(averageInterval)?.ElapsedMilliseconds) }, true )); diff --git a/src/SMAPI/Framework/PerformanceCounter/PeakEntry.cs b/src/SMAPI/Framework/PerformanceCounter/PeakEntry.cs new file mode 100644 index 00000000..95dc11f4 --- /dev/null +++ b/src/SMAPI/Framework/PerformanceCounter/PeakEntry.cs @@ -0,0 +1,24 @@ +using System; +using System.Collections.Generic; + +namespace StardewModdingAPI.Framework.PerformanceCounter +{ + internal struct PeakEntry + { + /// The actual execution time in milliseconds. + public readonly double ExecutionTimeMilliseconds; + + /// The DateTime when the entry occured. + public DateTime EventTime; + + /// The context list, which records all sources involved in exceeding the threshold. + public readonly List Context; + + public PeakEntry(double executionTimeMilliseconds, DateTime eventTime, List context) + { + this.ExecutionTimeMilliseconds = executionTimeMilliseconds; + this.EventTime = eventTime; + this.Context = context; + } + } +} diff --git a/src/SMAPI/Framework/PerformanceCounter/PerformanceCounter.cs b/src/SMAPI/Framework/PerformanceCounter/PerformanceCounter.cs index 33ddde2f..3d902e16 100644 --- a/src/SMAPI/Framework/PerformanceCounter/PerformanceCounter.cs +++ b/src/SMAPI/Framework/PerformanceCounter/PerformanceCounter.cs @@ -68,6 +68,26 @@ namespace StardewModdingAPI.Framework.PerformanceCounter return this.PeakPerformanceCounterEntry; } + /// Returns the peak entry. + /// The peak entry. + public PerformanceCounterEntry? GetPeak(TimeSpan range, DateTime? relativeTo = null) + { + if (this._counter.IsEmpty) + return null; + + if (relativeTo == null) + relativeTo = DateTime.UtcNow; + + DateTime start = relativeTo.Value.Subtract(range); + + var entries = this._counter.Where(x => (x.EventTime >= start) && (x.EventTime <= relativeTo)).ToList(); + + if (!entries.Any()) + return null; + + return entries.OrderByDescending(x => x.ElapsedMilliseconds).First(); + } + /// Resets the peak entry. public void ResetPeak() { diff --git a/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterCollection.cs b/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterCollection.cs index b48efd67..fe14ebf8 100644 --- a/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterCollection.cs +++ b/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterCollection.cs @@ -2,11 +2,15 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using Cyotek.Collections.Generic; namespace StardewModdingAPI.Framework.PerformanceCounter { internal class PerformanceCounterCollection { + /// The size of the ring buffer. + private const int MAX_ENTRIES = 16384; + /// The list of triggered performance counters. private readonly List TriggeredPerformanceCounters = new List(); @@ -22,6 +26,10 @@ namespace StardewModdingAPI.Framework.PerformanceCounter /// The number of invocations of this collection. private long CallCount; + /// The circular buffer which stores all peak invocations + private readonly CircularBuffer PeakInvocations; + + /// The associated performance counters. public IDictionary PerformanceCounters { get; } = new Dictionary(); /// The name of this collection. @@ -39,6 +47,7 @@ namespace StardewModdingAPI.Framework.PerformanceCounter public PerformanceCounterCollection(PerformanceCounterManager performanceCounterManager, string name, bool isImportant) { + this.PeakInvocations = new CircularBuffer(PerformanceCounterCollection.MAX_ENTRIES); this.Name = name; this.PerformanceCounterManager = performanceCounterManager; this.IsImportant = isImportant; @@ -46,6 +55,7 @@ namespace StardewModdingAPI.Framework.PerformanceCounter public PerformanceCounterCollection(PerformanceCounterManager performanceCounterManager, string name) { + this.PeakInvocations = new CircularBuffer(PerformanceCounterCollection.MAX_ENTRIES); this.PerformanceCounterManager = performanceCounterManager; this.Name = name; } @@ -89,15 +99,30 @@ namespace StardewModdingAPI.Framework.PerformanceCounter return 0; } + public double GetPeakExecutionTime(TimeSpan range, DateTime? relativeTo = null) + { + if (this.PeakInvocations.IsEmpty) + return 0; + + if (relativeTo == null) + relativeTo = DateTime.UtcNow; + + DateTime start = relativeTo.Value.Subtract(range); + + var entries = this.PeakInvocations.Where(x => (x.EventTime >= start) && (x.EventTime <= relativeTo)).ToList(); + + if (!entries.Any()) + return 0; + + return entries.OrderByDescending(x => x.ExecutionTimeMilliseconds).First().ExecutionTimeMilliseconds; + } + /// Begins tracking the invocation of this collection. public void BeginTrackInvocation() { - if (this.EnableAlerts) - { - this.TriggeredPerformanceCounters.Clear(); - this.InvocationStopwatch.Reset(); - this.InvocationStopwatch.Start(); - } + this.TriggeredPerformanceCounters.Clear(); + this.InvocationStopwatch.Reset(); + this.InvocationStopwatch.Start(); this.CallCount++; } @@ -106,10 +131,15 @@ namespace StardewModdingAPI.Framework.PerformanceCounter /// and the invocation time exceeds the threshold. public void EndTrackInvocation() { - if (!this.EnableAlerts) return; - this.InvocationStopwatch.Stop(); + this.PeakInvocations.Put( + new PeakEntry(this.InvocationStopwatch.Elapsed.TotalMilliseconds, + DateTime.UtcNow, + this.TriggeredPerformanceCounters)); + + if (!this.EnableAlerts) return; + if (this.InvocationStopwatch.Elapsed.TotalMilliseconds >= this.AlertThresholdMilliseconds) this.AddAlert(this.InvocationStopwatch.Elapsed.TotalMilliseconds, this.AlertThresholdMilliseconds, this.TriggeredPerformanceCounters); @@ -144,6 +174,7 @@ namespace StardewModdingAPI.Framework.PerformanceCounter /// Resets all performance counters in this collection. public void Reset() { + this.PeakInvocations.Clear(); foreach (var i in this.PerformanceCounters) i.Value.Reset(); } diff --git a/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterManager.cs b/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterManager.cs index 49720431..a8e20eda 100644 --- a/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterManager.cs +++ b/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterManager.cs @@ -33,6 +33,11 @@ namespace StardewModdingAPI.Framework.PerformanceCounter /// Resets all performance counters in all collections. public void Reset() { + foreach (PerformanceCounterCollection collection in this.PerformanceCounterCollections) + { + collection.Reset(); + } + foreach (var eventPerformanceCounter in this.PerformanceCounterCollections.SelectMany(performanceCounter => performanceCounter.PerformanceCounters)) { -- cgit From 25a22f5d7c527e60919b0e08a212578a323a8165 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 18 Jan 2020 16:21:16 -0500 Subject: update community links --- .github/CONTRIBUTING.md | 12 ++++-------- .github/SUPPORT.md | 3 +-- docs/release-notes.md | 1 + src/SMAPI.Web/Views/Index/Index.cshtml | 4 +++- src/SMAPI/Framework/SCore.cs | 2 +- 5 files changed, 10 insertions(+), 12 deletions(-) (limited to 'src') diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 8746a487..74a7c500 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -1,16 +1,12 @@ Do you want to... -* **Ask for help using SMAPI?** - Please ask in [the Stardew Valley Discord or mod forums](https://smapi.io/community), don't - create a GitHub issue. - -* **Report a bug?** - Please report it in [the Stardew Valley Discord or mod forums](https://smapi.io/community), don't - create a GitHub issue unless you're sure it's a bug in the SMAPI code. +* **Ask for help or report a bug?** + Please see 'get help' on [the SMAPI website](https://smapi.io) instead, don't create a GitHub + issue. * **Submit a pull request?** Pull requests are welcome! If you're submitting a new feature, it's best to discuss first to make - sure it'll be accepted. Feel free to come chat [on Discord or in the SMAPI discussion thread](https://smapi.io/community). + sure it'll be accepted. Feel free to come chat [on Discord](https://smapi.io/community). Documenting your code and using the same formatting conventions is appreciated, but don't worry too much about it. We'll fix up the code after we accept the pull request if needed. diff --git a/.github/SUPPORT.md b/.github/SUPPORT.md index 9263666f..cb968c30 100644 --- a/.github/SUPPORT.md +++ b/.github/SUPPORT.md @@ -1,4 +1,3 @@ GitHub issues are only used for SMAPI development tasks. -To get help with SMAPI problems, [ask on Discord or in the forums](https://smapi.io/community) -instead. +To get help with SMAPI problems, see 'get help' on [the SMAPI website](https://smapi.io/) instead. diff --git a/docs/release-notes.md b/docs/release-notes.md index fbf60573..68ef3d71 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -15,6 +15,7 @@ For modders: * Removed invalid-schedule validation which had false positives. For SMAPI/tool developers: + * Updated links for the new r/SMAPI subreddit. * The `/mods` web API endpoint now includes version mappings from the wiki. * Dropped API support for the pre-3.0 update-check format. diff --git a/src/SMAPI.Web/Views/Index/Index.cshtml b/src/SMAPI.Web/Views/Index/Index.cshtml index 778da2d1..eded9df3 100644 --- a/src/SMAPI.Web/Views/Index/Index.cshtml +++ b/src/SMAPI.Web/Views/Index/Index.cshtml @@ -53,9 +53,11 @@

Get help

+ (Or join the community!)
diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index b80f8ddf..d71b5e5a 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -308,7 +308,7 @@ namespace StardewModdingAPI.Framework // show details if game crashed during last session if (File.Exists(Constants.FatalCrashMarker)) { - this.Monitor.Log("The game crashed last time you played. That can be due to bugs in the game, but if it happens repeatedly you can ask for help here: https://community.playstarbound.com/threads/108375/.", LogLevel.Error); + this.Monitor.Log("The game crashed last time you played. If it happens repeatedly, see 'get help' on https://smapi.io.", LogLevel.Error); this.Monitor.Log("If you ask for help, make sure to share your SMAPI log: https://smapi.io/log.", LogLevel.Error); this.Monitor.Log("Press any key to delete the crash data and continue playing.", LogLevel.Info); Console.ReadKey(); -- cgit From 1b905205a3073c56e29c46b5e57c4a9cb2ca5832 Mon Sep 17 00:00:00 2001 From: Drachenkaetzchen Date: Tue, 21 Jan 2020 12:20:06 +0100 Subject: Added commands to enable and disable performance counters. Peak is now using the default interval --- .../Commands/Other/PerformanceCounterCommand.cs | 28 ++++++++++++++----- .../PerformanceCounter/PerformanceCounter.cs | 2 +- .../PerformanceCounterCollection.cs | 31 ++++++++++++++++++++++ .../PerformanceCounterManager.cs | 19 +++++++++++++ src/SMAPI/SMAPI.csproj | 4 +++ 5 files changed, 76 insertions(+), 8 deletions(-) (limited to 'src') diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/PerformanceCounterCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/PerformanceCounterCommand.cs index d49fc537..2260296b 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/PerformanceCounterCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/PerformanceCounterCommand.cs @@ -17,6 +17,8 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other {SubCommand.Detail, new[] {"detail", "d"}}, {SubCommand.Reset, new[] {"reset", "r"}}, {SubCommand.Trigger, new[] {"trigger"}}, + {SubCommand.Enable, new[] {"enable"}}, + {SubCommand.Disable, new[] {"disable"}}, {SubCommand.Examples, new[] {"examples"}}, {SubCommand.Concepts, new[] {"concepts"}}, {SubCommand.Help, new[] {"help"}}, @@ -29,6 +31,8 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other Detail, Reset, Trigger, + Enable, + Disable, Examples, Help, Concepts, @@ -69,6 +73,14 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other case SubCommand.Concepts: this.OutputHelp(monitor, SubCommand.Concepts); break; + case SubCommand.Enable: + SCore.PerformanceCounterManager.EnableTracking = true; + monitor.Log("Performance counter tracking is now enabled", LogLevel.Info); + break; + case SubCommand.Disable: + SCore.PerformanceCounterManager.EnableTracking = false; + monitor.Log("Performance counter tracking is now disabled", LogLevel.Info); + break; case SubCommand.Help: if (args.TryGet(1, "command", out string commandString)) this.OutputHelp(monitor, this.ParseCommandString(commandString)); @@ -118,20 +130,20 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other StringBuilder sb = new StringBuilder(); - TimeSpan peakSpan = TimeSpan.FromSeconds(60); + TimeSpan interval = TimeSpan.FromSeconds(60); - sb.AppendLine("Summary:"); + sb.AppendLine($"Summary over the last {interval.TotalSeconds} seconds:"); sb.AppendLine(this.GetTableString( data: data, - header: new[] {"Collection", "Avg Calls/s", "Avg Exec Time (Game)", "Avg Exec Time (Mods)", "Avg Exec Time (Game+Mods)", "Peak Exec Time (60s)"}, + header: new[] {"Collection", "Avg Calls/s", "Avg Exec Time (Game)", "Avg Exec Time (Mods)", "Avg Exec Time (Game+Mods)", "Peak Exec Time"}, getRow: item => new[] { item.Name, item.GetAverageCallsPerSecond().ToString(), - this.FormatMilliseconds(item.GetGameAverageExecutionTime(), threshold), - this.FormatMilliseconds(item.GetModsAverageExecutionTime(), threshold), - this.FormatMilliseconds(item.GetAverageExecutionTime(), threshold), - this.FormatMilliseconds(item.GetPeakExecutionTime(peakSpan), threshold) + this.FormatMilliseconds(item.GetGameAverageExecutionTime(interval), threshold), + this.FormatMilliseconds(item.GetModsAverageExecutionTime(interval), threshold), + this.FormatMilliseconds(item.GetAverageExecutionTime(interval), threshold), + this.FormatMilliseconds(item.GetPeakExecutionTime(interval), threshold) }, true )); @@ -662,6 +674,8 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other sb.AppendLine(" detail|d Shows performance counter information for a given collection"); sb.AppendLine(" reset|r Resets the performance counters"); sb.AppendLine(" trigger Configures alert triggers"); + sb.AppendLine(" enable Enables performance counter recording"); + sb.AppendLine(" disable Disables performance counter recording"); sb.AppendLine(" examples Displays various examples"); sb.AppendLine(" concepts Displays an explanation of the performance counter concepts"); sb.AppendLine(" help Displays verbose help for the available commands"); diff --git a/src/SMAPI/Framework/PerformanceCounter/PerformanceCounter.cs b/src/SMAPI/Framework/PerformanceCounter/PerformanceCounter.cs index 3d902e16..e9dfcb14 100644 --- a/src/SMAPI/Framework/PerformanceCounter/PerformanceCounter.cs +++ b/src/SMAPI/Framework/PerformanceCounter/PerformanceCounter.cs @@ -133,7 +133,7 @@ namespace StardewModdingAPI.Framework.PerformanceCounter DateTime start = relativeTo.Value.Subtract(range); - var entries = this._counter.Where(x => (x.EventTime >= start) && (x.EventTime <= relativeTo)); + var entries = this._counter.Where(x => (x.EventTime >= start) && (x.EventTime <= relativeTo)).ToList(); if (!entries.Any()) return 0; diff --git a/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterCollection.cs b/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterCollection.cs index fe14ebf8..f469eceb 100644 --- a/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterCollection.cs +++ b/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterCollection.cs @@ -82,6 +82,15 @@ namespace StardewModdingAPI.Framework.PerformanceCounter p.Key != Constants.GamePerformanceCounterName).Sum(p => p.Value.GetAverage()); } + /// Returns the average execution time for all non-game internal sources. + /// The interval for which to get the average, relative to now + /// The average execution time in milliseconds + public double GetModsAverageExecutionTime(TimeSpan interval) + { + return this.PerformanceCounters.Where(p => + p.Key != Constants.GamePerformanceCounterName).Sum(p => p.Value.GetAverage(interval)); + } + /// Returns the overall average execution time. /// The average execution time in milliseconds public double GetAverageExecutionTime() @@ -89,6 +98,14 @@ namespace StardewModdingAPI.Framework.PerformanceCounter return this.PerformanceCounters.Sum(p => p.Value.GetAverage()); } + /// Returns the overall average execution time. + /// The interval for which to get the average, relative to now + /// The average execution time in milliseconds + public double GetAverageExecutionTime(TimeSpan interval) + { + return this.PerformanceCounters.Sum(p => p.Value.GetAverage(interval)); + } + /// Returns the average execution time for game-internal sources. /// The average execution time in milliseconds public double GetGameAverageExecutionTime() @@ -99,6 +116,20 @@ namespace StardewModdingAPI.Framework.PerformanceCounter return 0; } + /// Returns the average execution time for game-internal sources. + /// The average execution time in milliseconds + public double GetGameAverageExecutionTime(TimeSpan interval) + { + if (this.PerformanceCounters.TryGetValue(Constants.GamePerformanceCounterName, out PerformanceCounter gameExecTime)) + return gameExecTime.GetAverage(interval); + + return 0; + } + + /// Returns the peak execution time + /// The interval for which to get the peak, relative to + /// The DateTime which the is relative to, or DateTime.Now if not given + /// The peak execution time public double GetPeakExecutionTime(TimeSpan range, DateTime? relativeTo = null) { if (this.PeakInvocations.IsEmpty) diff --git a/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterManager.cs b/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterManager.cs index a8e20eda..bd964442 100644 --- a/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterManager.cs +++ b/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterManager.cs @@ -23,6 +23,9 @@ namespace StardewModdingAPI.Framework.PerformanceCounter /// Specifies if alerts should be paused. public bool PauseAlerts { get; set; } + /// Specifies if performance counter tracking should be enabled. + public bool EnableTracking { get; set; } + /// Constructs a performance counter manager. /// The monitor for output logging. public PerformanceCounterManager(IMonitor monitor) @@ -49,6 +52,11 @@ namespace StardewModdingAPI.Framework.PerformanceCounter /// The collection name public void BeginTrackInvocation(string collectionName) { + if (!this.EnableTracking) + { + return; + } + this.GetOrCreateCollectionByName(collectionName).BeginTrackInvocation(); } @@ -56,6 +64,11 @@ namespace StardewModdingAPI.Framework.PerformanceCounter /// public void EndTrackInvocation(string collectionName) { + if (!this.EnableTracking) + { + return; + } + this.GetOrCreateCollectionByName(collectionName).EndTrackInvocation(); } @@ -65,6 +78,12 @@ namespace StardewModdingAPI.Framework.PerformanceCounter /// The action to execute and track invocation time for. public void Track(string collectionName, string sourceName, Action action) { + if (!this.EnableTracking) + { + action(); + return; + } + DateTime eventTime = DateTime.UtcNow; this.InvocationStopwatch.Reset(); this.InvocationStopwatch.Start(); diff --git a/src/SMAPI/SMAPI.csproj b/src/SMAPI/SMAPI.csproj index 5e407c2c..0bc290ac 100644 --- a/src/SMAPI/SMAPI.csproj +++ b/src/SMAPI/SMAPI.csproj @@ -15,6 +15,10 @@ icon.ico + + SMAPI_FOR_WINDOWS + + -- cgit From 1670a2f3a6263da158db5231f60d42d529734209 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 22 Jan 2020 19:06:33 -0500 Subject: fix global data stored in saves folder --- docs/release-notes.md | 1 + src/SMAPI.Installer/InteractiveInstaller.cs | 15 +++++++++++++++ src/SMAPI/Framework/ModHelpers/DataHelper.cs | 2 +- 3 files changed, 17 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index 68ef3d71..b7bd7b53 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -13,6 +13,7 @@ For modders: * Asset propagation for player sprites now affects other players' sprites, and updates recolor maps (e.g. sleeves). * Removed invalid-schedule validation which had false positives. + * Fixed `helper.Data.Read/WriteGlobalData` using the `Saves` folder. The installer will move existing folders to the appdata folder. For SMAPI/tool developers: * Updated links for the new r/SMAPI subreddit. diff --git a/src/SMAPI.Installer/InteractiveInstaller.cs b/src/SMAPI.Installer/InteractiveInstaller.cs index 964300ac..14f37258 100644 --- a/src/SMAPI.Installer/InteractiveInstaller.cs +++ b/src/SMAPI.Installer/InteractiveInstaller.cs @@ -373,6 +373,21 @@ namespace StardewModdingApi.Installer this.InteractivelyDelete(path); } + // move global save data folder (changed in 3.2) + { + string dataPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "StardewValley"); + DirectoryInfo oldDir = new DirectoryInfo(Path.Combine(dataPath, "Saves", ".smapi")); + DirectoryInfo newDir = new DirectoryInfo(Path.Combine(dataPath, ".smapi")); + + if (oldDir.Exists) + { + if (newDir.Exists) + this.InteractivelyDelete(oldDir.FullName); + else + oldDir.MoveTo(newDir.FullName); + } + } + /**** ** Install new files ****/ diff --git a/src/SMAPI/Framework/ModHelpers/DataHelper.cs b/src/SMAPI/Framework/ModHelpers/DataHelper.cs index 3d43c539..6cde849c 100644 --- a/src/SMAPI/Framework/ModHelpers/DataHelper.cs +++ b/src/SMAPI/Framework/ModHelpers/DataHelper.cs @@ -177,7 +177,7 @@ namespace StardewModdingAPI.Framework.ModHelpers private string GetGlobalDataPath(string key) { this.AssertSlug(key, nameof(key)); - return Path.Combine(Constants.SavesPath, ".smapi", "mod-data", this.ModID.ToLower(), $"{key}.json".ToLower()); + return Path.Combine(Constants.DataPath, ".smapi", "mod-data", this.ModID.ToLower(), $"{key}.json".ToLower()); } /// Assert that a key contains only characters that are safe in all contexts. -- cgit From 381de5eba9f9822c3483abdf64396cec794e3d03 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Wed, 22 Jan 2020 20:36:24 -0500 Subject: add test_input console command --- docs/release-notes.md | 14 +++-- .../Framework/Commands/ITrainerCommand.cs | 14 +++-- .../Framework/Commands/Other/TestInputCommand.cs | 59 ++++++++++++++++++++++ .../Framework/Commands/Player/SetHealthCommand.cs | 15 ++---- .../Framework/Commands/Player/SetMoneyCommand.cs | 13 ++--- .../Framework/Commands/Player/SetStaminaCommand.cs | 15 ++---- .../Framework/Commands/TrainerCommand.cs | 22 ++++++-- .../Framework/Commands/World/FreezeTimeCommand.cs | 15 ++---- src/SMAPI.Mods.ConsoleCommands/ModEntry.cs | 31 +++++++++--- 9 files changed, 134 insertions(+), 64 deletions(-) create mode 100644 src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/TestInputCommand.cs (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index b7bd7b53..8dac1d0c 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -10,6 +10,9 @@ * Fixed update-check error if a mod's Chucklefish page has no version. * Fixed SMAPI beta versions not showing update alert on next launch (thanks to danvolchek!). +For the Console Commands mod: + * Added `test_input` command to view button codes in the console. + For modders: * Asset propagation for player sprites now affects other players' sprites, and updates recolor maps (e.g. sleeves). * Removed invalid-schedule validation which had false positives. @@ -31,13 +34,14 @@ Released 05 January 2019 for Stardew Valley 1.4 or later. * Fixed compatibility with Linux Mint 18 (thanks to techge!), Arch Linux, and Linux systems with libhybris-utils installed. * Fixed memory leak when repeatedly loading a save and returning to title. * Fixed memory leak when mods reload assets. - * Fixes for Console Commands mod: - * added new clothing items; - * fixed spawning new flooring and rings (thanks to Mizzion!); - * fixed spawning custom rings added by mods; - * Fixed errors when some item data is invalid. * Updated translations. Thanks to L30Bola (added Portuguese), PlussRolf (added Spanish), and shirutan (added Japanese)! +* For the Console Commands mod: + * Added new clothing items. + * Fixed spawning new flooring and rings (thanks to Mizzion!). + * Fixed spawning custom rings added by mods. + * Fixed errors when some item data is invalid. + * For the web UI: * Added option to edit & reupload in the JSON validator. * File uploads are now stored in Azure storage instead of Pastebin, due to ongoing Pastebin perfomance issues. diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/ITrainerCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/ITrainerCommand.cs index a0b739f8..d4d36e5d 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/ITrainerCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/ITrainerCommand.cs @@ -12,8 +12,11 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands /// The command description. string Description { get; } - /// Whether the command needs to perform logic when the game updates. - bool NeedsUpdate { get; } + /// Whether the command may need to perform logic when the game updates. This value shouldn't change. + bool MayNeedUpdate { get; } + + /// Whether the command may need to perform logic when the player presses a button. This value shouldn't change. + bool MayNeedInput { get; } /********* @@ -27,6 +30,11 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands /// Perform any logic needed on update tick. /// Writes messages to the console and log file. - void Update(IMonitor monitor); + void OnUpdated(IMonitor monitor); + + /// Perform any logic when input is received. + /// Writes messages to the console and log file. + /// The button that was pressed. + void OnButtonPressed(IMonitor monitor, SButton button); } } diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/TestInputCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/TestInputCommand.cs new file mode 100644 index 00000000..11aa10c3 --- /dev/null +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/TestInputCommand.cs @@ -0,0 +1,59 @@ +using System; + +namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other +{ + /// A command which logs the keys being pressed for 30 seconds once enabled. + internal class TestInputCommand : TrainerCommand + { + /********* + ** Fields + *********/ + /// The number of seconds for which to log input. + private readonly int LogSeconds = 30; + + /// When the command should stop printing input, or null if currently disabled. + private long? ExpiryTicks; + + + /********* + ** Public methods + *********/ + /// Construct an instance. + public TestInputCommand() + : base("test_input", "Prints all input to the console for 30 seconds.", mayNeedUpdate: true, mayNeedInput: true) { } + + /// Handle the command. + /// Writes messages to the console and log file. + /// The command name. + /// The command arguments. + public override void Handle(IMonitor monitor, string command, ArgumentParser args) + { + this.ExpiryTicks = DateTime.UtcNow.Add(TimeSpan.FromSeconds(this.LogSeconds)).Ticks; + monitor.Log($"OK, logging all player input for {this.LogSeconds} seconds.", LogLevel.Info); + } + + /// Perform any logic needed on update tick. + /// Writes messages to the console and log file. + public override void OnUpdated(IMonitor monitor) + { + // handle expiry + if (this.ExpiryTicks == null) + return; + if (this.ExpiryTicks <= DateTime.UtcNow.Ticks) + { + monitor.Log("No longer logging input.", LogLevel.Info); + this.ExpiryTicks = null; + return; + } + } + + /// Perform any logic when input is received. + /// Writes messages to the console and log file. + /// The button that was pressed. + public override void OnButtonPressed(IMonitor monitor, SButton button) + { + if (this.ExpiryTicks != null) + monitor.Log($"Pressed {button}", LogLevel.Info); + } + } +} diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetHealthCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetHealthCommand.cs index 1abb82b5..59bda5dd 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetHealthCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetHealthCommand.cs @@ -1,4 +1,4 @@ -using System.Linq; +using System.Linq; using StardewValley; namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player @@ -13,19 +13,12 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player private bool InfiniteHealth; - /********* - ** Accessors - *********/ - /// Whether the command needs to perform logic when the game updates. - public override bool NeedsUpdate => this.InfiniteHealth; - - /********* ** Public methods *********/ /// Construct an instance. public SetHealthCommand() - : base("player_sethealth", "Sets the player's health.\n\nUsage: player_sethealth [value]\n- value: an integer amount, or 'inf' for infinite health.") { } + : base("player_sethealth", "Sets the player's health.\n\nUsage: player_sethealth [value]\n- value: an integer amount, or 'inf' for infinite health.", mayNeedUpdate: true) { } /// Handle the command. /// Writes messages to the console and log file. @@ -62,9 +55,9 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player /// Perform any logic needed on update tick. /// Writes messages to the console and log file. - public override void Update(IMonitor monitor) + public override void OnUpdated(IMonitor monitor) { - if (this.InfiniteHealth) + if (this.InfiniteHealth && Context.IsWorldReady) Game1.player.health = Game1.player.maxHealth; } } diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetMoneyCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetMoneyCommand.cs index 1706bbc1..6e3d68b6 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetMoneyCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetMoneyCommand.cs @@ -13,19 +13,12 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player private bool InfiniteMoney; - /********* - ** Accessors - *********/ - /// Whether the command needs to perform logic when the game updates. - public override bool NeedsUpdate => this.InfiniteMoney; - - /********* ** Public methods *********/ /// Construct an instance. public SetMoneyCommand() - : base("player_setmoney", "Sets the player's money.\n\nUsage: player_setmoney \n- value: an integer amount, or 'inf' for infinite money.") { } + : base("player_setmoney", "Sets the player's money.\n\nUsage: player_setmoney \n- value: an integer amount, or 'inf' for infinite money.", mayNeedUpdate: true) { } /// Handle the command. /// Writes messages to the console and log file. @@ -62,9 +55,9 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player /// Perform any logic needed on update tick. /// Writes messages to the console and log file. - public override void Update(IMonitor monitor) + public override void OnUpdated(IMonitor monitor) { - if (this.InfiniteMoney) + if (this.InfiniteMoney && Context.IsWorldReady) Game1.player.Money = 999999; } } diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetStaminaCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetStaminaCommand.cs index 009cb1de..60a1dcb1 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetStaminaCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Player/SetStaminaCommand.cs @@ -1,4 +1,4 @@ -using System.Linq; +using System.Linq; using StardewValley; namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player @@ -13,19 +13,12 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player private bool InfiniteStamina; - /********* - ** Accessors - *********/ - /// Whether the command needs to perform logic when the game updates. - public override bool NeedsUpdate => this.InfiniteStamina; - - /********* ** Public methods *********/ /// Construct an instance. public SetStaminaCommand() - : base("player_setstamina", "Sets the player's stamina.\n\nUsage: player_setstamina [value]\n- value: an integer amount, or 'inf' for infinite stamina.") { } + : base("player_setstamina", "Sets the player's stamina.\n\nUsage: player_setstamina [value]\n- value: an integer amount, or 'inf' for infinite stamina.", mayNeedUpdate: true) { } /// Handle the command. /// Writes messages to the console and log file. @@ -62,9 +55,9 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Player /// Perform any logic needed on update tick. /// Writes messages to the console and log file. - public override void Update(IMonitor monitor) + public override void OnUpdated(IMonitor monitor) { - if (this.InfiniteStamina) + if (this.InfiniteStamina && Context.IsWorldReady) Game1.player.stamina = Game1.player.MaxStamina; } } diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/TrainerCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/TrainerCommand.cs index 466b8f6e..6d5cae97 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/TrainerCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/TrainerCommand.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; @@ -16,8 +16,11 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands /// The command description. public string Description { get; } - /// Whether the command needs to perform logic when the game updates. - public virtual bool NeedsUpdate { get; } = false; + /// Whether the command may need to perform logic when the player presses a button. This value shouldn't change. + public bool MayNeedInput { get; } + + /// Whether the command may need to perform logic when the game updates. This value shouldn't change. + public bool MayNeedUpdate { get; } /********* @@ -31,7 +34,12 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands /// Perform any logic needed on update tick. /// Writes messages to the console and log file. - public virtual void Update(IMonitor monitor) { } + public virtual void OnUpdated(IMonitor monitor) { } + + /// Perform any logic when input is received. + /// Writes messages to the console and log file. + /// The button that was pressed. + public virtual void OnButtonPressed(IMonitor monitor, SButton button) { } /********* @@ -40,10 +48,14 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands /// Construct an instance. /// The command name the user must type. /// The command description. - protected TrainerCommand(string name, string description) + /// Whether the command may need to perform logic when the player presses a button. + /// Whether the command may need to perform logic when the game updates. + protected TrainerCommand(string name, string description, bool mayNeedInput = false, bool mayNeedUpdate = false) { this.Name = name; this.Description = description; + this.MayNeedInput = mayNeedInput; + this.MayNeedUpdate = mayNeedUpdate; } /// Log an error indicating incorrect usage. diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/FreezeTimeCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/FreezeTimeCommand.cs index 6a7ab162..736a93a0 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/FreezeTimeCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/World/FreezeTimeCommand.cs @@ -1,4 +1,4 @@ -using System.Linq; +using System.Linq; using StardewValley; namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World @@ -16,19 +16,12 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World private bool FreezeTime; - /********* - ** Accessors - *********/ - /// Whether the command needs to perform logic when the game updates. - public override bool NeedsUpdate => this.FreezeTime; - - /********* ** Public methods *********/ /// Construct an instance. public FreezeTimeCommand() - : base("world_freezetime", "Freezes or resumes time.\n\nUsage: world_freezetime [value]\n- value: one of 0 (resume), 1 (freeze), or blank (toggle).") { } + : base("world_freezetime", "Freezes or resumes time.\n\nUsage: world_freezetime [value]\n- value: one of 0 (resume), 1 (freeze), or blank (toggle).", mayNeedUpdate: true) { } /// Handle the command. /// Writes messages to the console and log file. @@ -57,9 +50,9 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.World /// Perform any logic needed on update tick. /// Writes messages to the console and log file. - public override void Update(IMonitor monitor) + public override void OnUpdated(IMonitor monitor) { - if (this.FreezeTime) + if (this.FreezeTime && Context.IsWorldReady) Game1.timeOfDay = FreezeTimeCommand.FrozenTime; } } diff --git a/src/SMAPI.Mods.ConsoleCommands/ModEntry.cs b/src/SMAPI.Mods.ConsoleCommands/ModEntry.cs index 4807c46d..5c4f3bba 100644 --- a/src/SMAPI.Mods.ConsoleCommands/ModEntry.cs +++ b/src/SMAPI.Mods.ConsoleCommands/ModEntry.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using StardewModdingAPI.Events; using StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands; namespace StardewModdingAPI.Mods.ConsoleCommands @@ -14,6 +15,12 @@ namespace StardewModdingAPI.Mods.ConsoleCommands /// The commands to handle. private ITrainerCommand[] Commands; + /// The commands which may need to handle update ticks. + private ITrainerCommand[] UpdateHandlers; + + /// The commands which may need to handle input. + private ITrainerCommand[] InputHandlers; + /********* ** Public methods @@ -27,27 +34,35 @@ namespace StardewModdingAPI.Mods.ConsoleCommands foreach (ITrainerCommand command in this.Commands) helper.ConsoleCommands.Add(command.Name, command.Description, (name, args) => this.HandleCommand(command, name, args)); + // cache commands + this.InputHandlers = this.Commands.Where(p => p.MayNeedInput).ToArray(); + this.UpdateHandlers = this.Commands.Where(p => p.MayNeedUpdate).ToArray(); + // hook events helper.Events.GameLoop.UpdateTicked += this.OnUpdateTicked; + helper.Events.Input.ButtonPressed += this.OnButtonPressed; } /********* ** Private methods *********/ + /// The method invoked when a button is pressed. + /// The event sender. + /// The event arguments. + private void OnButtonPressed(object sender, ButtonPressedEventArgs e) + { + foreach (ITrainerCommand command in this.InputHandlers) + command.OnButtonPressed(this.Monitor, e.Button); + } + /// The method invoked when the game updates its state. /// The event sender. /// The event arguments. private void OnUpdateTicked(object sender, EventArgs e) { - if (!Context.IsWorldReady) - return; - - foreach (ITrainerCommand command in this.Commands) - { - if (command.NeedsUpdate) - command.Update(this.Monitor); - } + foreach (ITrainerCommand command in this.UpdateHandlers) + command.OnUpdated(this.Monitor); } /// Handle a console command. -- cgit From d1935e686c6396519a1ff9b1b429cd55adcf8d11 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 23 Jan 2020 00:31:26 -0500 Subject: add full internal support for non-standard four-part versions --- docs/release-notes.md | 7 +- src/SMAPI.Tests/Utilities/SemanticVersionTests.cs | 208 ++++++++++++++++----- .../ISemanticVersion.cs | 3 + .../Framework/SemanticVersionReader.cs | 126 +++++++++++++ src/SMAPI.Toolkit/SemanticVersion.cs | 72 ++++--- .../Converters/SemanticVersionConverter.cs | 2 +- src/SMAPI.Web/Framework/LogParsing/LogParser.cs | 9 +- src/SMAPI.Web/Framework/VersionConstraint.cs | 28 ++- src/SMAPI/Framework/GameVersion.cs | 30 +-- src/SMAPI/SemanticVersion.cs | 34 +++- 10 files changed, 407 insertions(+), 112 deletions(-) create mode 100644 src/SMAPI.Toolkit/Framework/SemanticVersionReader.cs (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index 8dac1d0c..35fdbb4b 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -10,15 +10,16 @@ * Fixed update-check error if a mod's Chucklefish page has no version. * Fixed SMAPI beta versions not showing update alert on next launch (thanks to danvolchek!). -For the Console Commands mod: +* For the Console Commands mod: * Added `test_input` command to view button codes in the console. -For modders: +* For modders: * Asset propagation for player sprites now affects other players' sprites, and updates recolor maps (e.g. sleeves). * Removed invalid-schedule validation which had false positives. * Fixed `helper.Data.Read/WriteGlobalData` using the `Saves` folder. The installer will move existing folders to the appdata folder. -For SMAPI/tool developers: +* For SMAPI/tool developers: + * Added internal support for four-part versions to support SMAPI on Android. * Updated links for the new r/SMAPI subreddit. * The `/mods` web API endpoint now includes version mappings from the wiki. * Dropped API support for the pre-3.0 update-check format. diff --git a/src/SMAPI.Tests/Utilities/SemanticVersionTests.cs b/src/SMAPI.Tests/Utilities/SemanticVersionTests.cs index 48afcaa2..ac4ef39b 100644 --- a/src/SMAPI.Tests/Utilities/SemanticVersionTests.cs +++ b/src/SMAPI.Tests/Utilities/SemanticVersionTests.cs @@ -17,7 +17,8 @@ namespace SMAPI.Tests.Utilities /**** ** Constructor ****/ - [Test(Description = "Assert that the constructor sets the expected values for all valid versions when constructed from a string.")] + /// Assert the parsed version when constructed from a standard string. + /// The version string to parse. [TestCase("1.0", ExpectedResult = "1.0.0")] [TestCase("1.0.0", ExpectedResult = "1.0.0")] [TestCase("3000.4000.5000", ExpectedResult = "3000.4000.5000")] @@ -29,10 +30,76 @@ namespace SMAPI.Tests.Utilities [TestCase("1.2+3.4.5-build.004", ExpectedResult = "1.2.0+3.4.5-build.004")] public string Constructor_FromString(string input) { - return new SemanticVersion(input).ToString(); + // act + ISemanticVersion version = new SemanticVersion(input); + + // assert + return version.ToString(); } - [Test(Description = "Assert that the constructor sets the expected values for all valid versions when constructed from the individual numbers.")] + + /// Assert that the constructor rejects invalid values when constructed from a string. + /// The version string to parse. + [Test(Description = "Assert that the constructor throws the expected exception for invalid versions.")] + [TestCase(null)] + [TestCase("")] + [TestCase(" ")] + [TestCase("1")] + [TestCase("01.0")] + [TestCase("1.05")] + [TestCase("1.5.06")] // leading zeros specifically prohibited by spec + [TestCase("1.2.3.4")] + [TestCase("1.apple")] + [TestCase("1.2.apple")] + [TestCase("1.2.3.apple")] + [TestCase("1..2..3")] + [TestCase("1.2.3-")] + [TestCase("1.2.3--some-tag")] + [TestCase("1.2.3-some-tag...")] + [TestCase("1.2.3-some-tag...4")] + [TestCase("1.2.3-some-tag.4+build...4")] + [TestCase("apple")] + [TestCase("-apple")] + [TestCase("-5")] + public void Constructor_FromString_WithInvalidValues(string input) + { + if (input == null) + this.AssertAndLogException(() => new SemanticVersion(input)); + else + this.AssertAndLogException(() => new SemanticVersion(input)); + } + + /// Assert the parsed version when constructed from a non-standard string. + /// The version string to parse. + [TestCase("1.2.3", ExpectedResult = "1.2.3")] + [TestCase("1.0.0.0", ExpectedResult = "1.0.0")] + [TestCase("1.0.0.5", ExpectedResult = "1.0.0.5")] + [TestCase("1.2.3.4-some-tag.4 ", ExpectedResult = "1.2.3.4-some-tag.4")] + public string Constructor_FromString_NonStandard(string input) + { + // act + ISemanticVersion version = new SemanticVersion(input, allowNonStandard: true); + + // assert + return version.ToString(); + } + + /// Assert that the constructor rejects a non-standard string when the non-standard flag isn't set. + /// The version string to parse. + [TestCase("1.0.0.0")] + [TestCase("1.0.0.5")] + [TestCase("1.2.3.4-some-tag.4 ")] + public void Constructor_FromString_Standard_DisallowsNonStandardVersion(string input) + { + Assert.Throws(() => new SemanticVersion(input)); + } + + /// Assert the parsed version when constructed from standard parts. + /// The major number. + /// The minor number. + /// The patch number. + /// The prerelease tag. + /// The build metadata. [TestCase(1, 0, 0, null, null, ExpectedResult = "1.0.0")] [TestCase(3000, 4000, 5000, null, null, ExpectedResult = "3000.4000.5000")] [TestCase(1, 2, 3, "", null, ExpectedResult = "1.2.3")] @@ -49,15 +116,43 @@ namespace SMAPI.Tests.Utilities ISemanticVersion version = new SemanticVersion(major, minor, patch, prerelease, build); // assert - Assert.AreEqual(major, version.MajorVersion, "The major version doesn't match the given value."); - Assert.AreEqual(minor, version.MinorVersion, "The minor version doesn't match the given value."); - Assert.AreEqual(patch, version.PatchVersion, "The patch version doesn't match the given value."); - Assert.AreEqual(string.IsNullOrWhiteSpace(prerelease) ? null : prerelease.Trim(), version.PrereleaseTag, "The prerelease tag doesn't match the given value."); - Assert.AreEqual(string.IsNullOrWhiteSpace(build) ? null : build.Trim(), version.BuildMetadata, "The build metadata doesn't match the given value."); + this.AssertParts(version, major, minor, patch, prerelease, build, nonStandard: false); return version.ToString(); } - [Test(Description = "Assert that the constructor throws the expected exception for invalid versions when constructed from the individual numbers.")] + /// Assert the parsed version when constructed from parts including non-standard fields. + /// The major number. + /// The minor number. + /// The patch number. + /// The non-standard platform release number. + /// The prerelease tag. + /// The build metadata. + [TestCase(1, 0, 0, 0, null, null, ExpectedResult = "1.0.0")] + [TestCase(3000, 4000, 5000, 6000, null, null, ExpectedResult = "3000.4000.5000.6000")] + [TestCase(1, 2, 3, 4, "", null, ExpectedResult = "1.2.3.4")] + [TestCase(1, 2, 3, 4, " ", null, ExpectedResult = "1.2.3.4")] + [TestCase(1, 2, 3, 4, "0", null, ExpectedResult = "1.2.3.4-0")] + [TestCase(1, 2, 3, 4, "some-tag.4", null, ExpectedResult = "1.2.3.4-some-tag.4")] + [TestCase(1, 2, 3, 4, "sOMe-TaG.4", null, ExpectedResult = "1.2.3.4-sOMe-TaG.4")] + [TestCase(1, 2, 3, 4, "some-tag.4 ", null, ExpectedResult = "1.2.3.4-some-tag.4")] + [TestCase(1, 2, 3, 4, "some-tag.4 ", "build.004", ExpectedResult = "1.2.3.4-some-tag.4+build.004")] + [TestCase(1, 2, 0, 4, null, "3.4.5-build.004", ExpectedResult = "1.2.0.4+3.4.5-build.004")] + public string Constructor_FromParts_NonStandard(int major, int minor, int patch, int platformRelease, string prerelease, string build) + { + // act + ISemanticVersion version = new SemanticVersion(major, minor, patch, platformRelease, prerelease, build); + + // assert + this.AssertParts(version, major, minor, patch, prerelease, build, nonStandard: platformRelease != 0); + return version.ToString(); + } + + /// Assert that the constructor rejects invalid values when constructed from the individual numbers. + /// The major number. + /// The minor number. + /// The patch number. + /// The prerelease tag. + /// The build metadata. [TestCase(0, 0, 0, null, null)] [TestCase(-1, 0, 0, null, null)] [TestCase(0, -1, 0, null, null)] @@ -71,6 +166,10 @@ namespace SMAPI.Tests.Utilities this.AssertAndLogException(() => new SemanticVersion(major, minor, patch, prerelease, build)); } + /// Assert the parsed version when constructed from an assembly version. + /// The major number. + /// The minor number. + /// The patch number. [Test(Description = "Assert that the constructor sets the expected values for all valid versions when constructed from an assembly version.")] [TestCase(1, 0, 0, ExpectedResult = "1.0.0")] [TestCase(1, 2, 3, ExpectedResult = "1.2.3")] @@ -81,45 +180,16 @@ namespace SMAPI.Tests.Utilities ISemanticVersion version = new SemanticVersion(new Version(major, minor, patch)); // assert - Assert.AreEqual(major, version.MajorVersion, "The major version doesn't match the given value."); - Assert.AreEqual(minor, version.MinorVersion, "The minor version doesn't match the given value."); - Assert.AreEqual(patch, version.PatchVersion, "The patch version doesn't match the given value."); + this.AssertParts(version, major, minor, patch, null, null, nonStandard: false); return version.ToString(); } - [Test(Description = "Assert that the constructor throws the expected exception for invalid versions.")] - [TestCase(null)] - [TestCase("")] - [TestCase(" ")] - [TestCase("1")] - [TestCase("01.0")] - [TestCase("1.05")] - [TestCase("1.5.06")] // leading zeros specifically prohibited by spec - [TestCase("1.2.3.4")] - [TestCase("1.apple")] - [TestCase("1.2.apple")] - [TestCase("1.2.3.apple")] - [TestCase("1..2..3")] - [TestCase("1.2.3-")] - [TestCase("1.2.3--some-tag")] - [TestCase("1.2.3-some-tag...")] - [TestCase("1.2.3-some-tag...4")] - [TestCase("1.2.3-some-tag.4+build...4")] - [TestCase("apple")] - [TestCase("-apple")] - [TestCase("-5")] - public void Constructor_FromString_WithInvalidValues(string input) - { - if (input == null) - this.AssertAndLogException(() => new SemanticVersion(input)); - else - this.AssertAndLogException(() => new SemanticVersion(input)); - } - /**** ** CompareTo ****/ - [Test(Description = "Assert that version.CompareTo returns the expected value.")] + /// Assert that returns the expected value. + /// The left version. + /// The right version. // equal [TestCase("0.5.7", "0.5.7", ExpectedResult = 0)] [TestCase("1.0", "1.0", ExpectedResult = 0)] @@ -149,15 +219,20 @@ namespace SMAPI.Tests.Utilities [TestCase("1.0-beta-10", "1.0-beta-2", ExpectedResult = 1)] public int CompareTo(string versionStrA, string versionStrB) { + // arrange ISemanticVersion versionA = new SemanticVersion(versionStrA); ISemanticVersion versionB = new SemanticVersion(versionStrB); + + // assert return versionA.CompareTo(versionB); } /**** ** IsOlderThan ****/ - [Test(Description = "Assert that version.IsOlderThan returns the expected value.")] + /// Assert that and return the expected value. + /// The left version. + /// The right version. // keep test cases in sync with CompareTo for simplicity. // equal [TestCase("0.5.7", "0.5.7", ExpectedResult = false)] @@ -187,15 +262,21 @@ namespace SMAPI.Tests.Utilities [TestCase("1.0-beta-10", "1.0-beta-2", ExpectedResult = false)] public bool IsOlderThan(string versionStrA, string versionStrB) { + // arrange ISemanticVersion versionA = new SemanticVersion(versionStrA); ISemanticVersion versionB = new SemanticVersion(versionStrB); + + // assert + Assert.AreEqual(versionA.IsOlderThan(versionB), versionA.IsOlderThan(versionB.ToString()), "The two signatures returned different results."); return versionA.IsOlderThan(versionB); } /**** ** IsNewerThan ****/ - [Test(Description = "Assert that version.IsNewerThan returns the expected value.")] + /// Assert that and return the expected value. + /// The left version. + /// The right version. // keep test cases in sync with CompareTo for simplicity. // equal [TestCase("0.5.7", "0.5.7", ExpectedResult = false)] @@ -225,14 +306,22 @@ namespace SMAPI.Tests.Utilities [TestCase("1.0-beta-10", "1.0-beta-2", ExpectedResult = true)] public bool IsNewerThan(string versionStrA, string versionStrB) { + // arrange ISemanticVersion versionA = new SemanticVersion(versionStrA); ISemanticVersion versionB = new SemanticVersion(versionStrB); + + // assert + Assert.AreEqual(versionA.IsNewerThan(versionB), versionA.IsNewerThan(versionB.ToString()), "The two signatures returned different results."); return versionA.IsNewerThan(versionB); } /**** ** IsBetween ****/ + /// Assert that and return the expected value. + /// The main version. + /// The lower version number. + /// The upper version number. [Test(Description = "Assert that version.IsNewerThan returns the expected value.")] // is between [TestCase("0.5.7-beta.3", "0.5.7-beta.3", "0.5.7-beta.3", ExpectedResult = true)] @@ -250,17 +339,24 @@ namespace SMAPI.Tests.Utilities [TestCase("1.0-beta-2", "1.0-beta-10", "1.0-beta-3", ExpectedResult = false)] public bool IsBetween(string versionStr, string lowerStr, string upperStr) { + // arrange ISemanticVersion lower = new SemanticVersion(lowerStr); ISemanticVersion upper = new SemanticVersion(upperStr); ISemanticVersion version = new SemanticVersion(versionStr); + + // assert + Assert.AreEqual(version.IsBetween(lower, upper), version.IsBetween(lower.ToString(), upper.ToString()), "The two signatures returned different results."); return version.IsBetween(lower, upper); } /**** ** Serializable ****/ - [Test(Description = "Assert that SemanticVersion can be round-tripped through JSON with no special configuration.")] + /// Assert that the version can be round-tripped through JSON with no special configuration. + /// The semantic version. [TestCase("1.0.0")] + [TestCase("1.0.0-beta.400")] + [TestCase("1.0.0-beta.400+build")] public void Serializable(string versionStr) { // act @@ -272,10 +368,12 @@ namespace SMAPI.Tests.Utilities Assert.AreEqual(versionStr, after.ToString(), "The semantic version after deserialization doesn't match the input version."); } + /**** ** GameVersion ****/ - [Test(Description = "Assert that the GameVersion subclass correctly parses legacy game versions.")] + /// Assert that the GameVersion subclass correctly parses non-standard game versions. + /// The raw version. [TestCase("1.0")] [TestCase("1.01")] [TestCase("1.02")] @@ -307,6 +405,24 @@ namespace SMAPI.Tests.Utilities /********* ** Private methods *********/ + /// Assert that the version matches the expected parts. + /// The version number. + /// The major number. + /// The minor number. + /// The patch number. + /// The prerelease tag. + /// The build metadata. + /// Whether the version should be marked as non-standard. + private void AssertParts(ISemanticVersion version, int major, int minor, int patch, string prerelease, string build, bool nonStandard) + { + Assert.AreEqual(major, version.MajorVersion, "The major version doesn't match."); + Assert.AreEqual(minor, version.MinorVersion, "The minor version doesn't match."); + Assert.AreEqual(patch, version.PatchVersion, "The patch version doesn't match."); + Assert.AreEqual(string.IsNullOrWhiteSpace(prerelease) ? null : prerelease.Trim(), version.PrereleaseTag, "The prerelease tag doesn't match."); + Assert.AreEqual(string.IsNullOrWhiteSpace(build) ? null : build.Trim(), version.BuildMetadata, "The build metadata doesn't match."); + Assert.AreEqual(nonStandard, version.IsNonStandard(), $"The version is incorrectly marked {(nonStandard ? "standard" : "non-standard")}."); + } + /// Assert that the expected exception type is thrown, and log the action output and thrown exception. /// The expected exception type. /// The action which may throw the exception. diff --git a/src/SMAPI.Toolkit.CoreInterfaces/ISemanticVersion.cs b/src/SMAPI.Toolkit.CoreInterfaces/ISemanticVersion.cs index b8572d50..b228b2d1 100644 --- a/src/SMAPI.Toolkit.CoreInterfaces/ISemanticVersion.cs +++ b/src/SMAPI.Toolkit.CoreInterfaces/ISemanticVersion.cs @@ -61,5 +61,8 @@ namespace StardewModdingAPI /// Get a string representation of the version. string ToString(); + + /// Whether the version uses non-standard extensions, like four-part game versions on some platforms. + bool IsNonStandard(); } } diff --git a/src/SMAPI.Toolkit/Framework/SemanticVersionReader.cs b/src/SMAPI.Toolkit/Framework/SemanticVersionReader.cs new file mode 100644 index 00000000..489e1c4d --- /dev/null +++ b/src/SMAPI.Toolkit/Framework/SemanticVersionReader.cs @@ -0,0 +1,126 @@ +namespace StardewModdingAPI.Toolkit.Framework +{ + /// Reads strings into a semantic version. + internal static class SemanticVersionReader + { + /********* + ** Public methods + *********/ + /// Parse a semantic version string. + /// The version string to parse. + /// Whether to recognize non-standard semver extensions. + /// The major version incremented for major API changes. + /// The minor version incremented for backwards-compatible changes. + /// The patch version for backwards-compatible fixes. + /// The platform-specific version (if applicable). + /// An optional prerelease tag. + /// Optional build metadata. This is ignored when determining version precedence. + /// Returns whether the version was successfully parsed. + public static bool TryParse(string versionStr, bool allowNonStandard, out int major, out int minor, out int patch, out int platformRelease, out string prereleaseTag, out string buildMetadata) + { + // init + major = 0; + minor = 0; + patch = 0; + platformRelease = 0; + prereleaseTag = null; + buildMetadata = null; + + // normalize + versionStr = versionStr?.Trim(); + if (string.IsNullOrWhiteSpace(versionStr)) + return false; + char[] raw = versionStr.ToCharArray(); + + // read major/minor version + int i = 0; + if (!TryParseVersionPart(raw, ref i, out major) || !TryParseLiteral(raw, ref i, '.') || !TryParseVersionPart(raw, ref i, out minor)) + return false; + + // read optional patch version + if (TryParseLiteral(raw, ref i, '.') && !TryParseVersionPart(raw, ref i, out patch)) + return false; + + // read optional non-standard platform release version + if (allowNonStandard && TryParseLiteral(raw, ref i, '.') && !TryParseVersionPart(raw, ref i, out platformRelease)) + return false; + + // read optional prerelease tag + if (TryParseLiteral(raw, ref i, '-') && !TryParseTag(raw, ref i, out prereleaseTag)) + return false; + + // read optional build tag + if (TryParseLiteral(raw, ref i, '+') && !TryParseTag(raw, ref i, out buildMetadata)) + return false; + + // validate + return i == versionStr.Length; // valid if we're at the end + } + + + /********* + ** Private methods + *********/ + /// Try to parse the next characters in a queue as a numeric part. + /// The raw characters to parse. + /// The index of the next character to read. + /// The parsed part. + private static bool TryParseVersionPart(char[] raw, ref int index, out int part) + { + part = 0; + + // take digits + string str = ""; + for (int i = index; i < raw.Length && char.IsDigit(raw[i]); i++) + str += raw[i]; + + // validate + if (str.Length == 0) + return false; + if (str.Length > 1 && str[0] == '0') + return false; // can't have leading zeros + + // parse + part = int.Parse(str); + index += str.Length; + return true; + } + + /// Try to parse a literal character. + /// The raw characters to parse. + /// The index of the next character to read. + /// The expected character. + private static bool TryParseLiteral(char[] raw, ref int index, char ch) + { + if (index >= raw.Length || raw[index] != ch) + return false; + + index++; + return true; + } + + /// Try to parse a tag. + /// The raw characters to parse. + /// The index of the next character to read. + /// The parsed tag. + private static bool TryParseTag(char[] raw, ref int index, out string tag) + { + // read tag length + int length = 0; + for (int i = index; i < raw.Length && (char.IsLetterOrDigit(raw[i]) || raw[i] == '-' || raw[i] == '.'); i++) + length++; + + // validate + if (length == 0) + { + tag = null; + return false; + } + + // parse + tag = new string(raw, index, length); + index += length; + return true; + } + } +} diff --git a/src/SMAPI.Toolkit/SemanticVersion.cs b/src/SMAPI.Toolkit/SemanticVersion.cs index 4955dcae..5ead6dc8 100644 --- a/src/SMAPI.Toolkit/SemanticVersion.cs +++ b/src/SMAPI.Toolkit/SemanticVersion.cs @@ -1,5 +1,6 @@ using System; using System.Text.RegularExpressions; +using StardewModdingAPI.Toolkit.Framework; namespace StardewModdingAPI.Toolkit { @@ -9,6 +10,8 @@ namespace StardewModdingAPI.Toolkit /// - short-form "x.y" versions are supported (equivalent to "x.y.0"); /// - hyphens are synonymous with dots in prerelease tags and build metadata (like "-unofficial.3-pathoschild"); /// - and "-unofficial" in prerelease tags is always lower-precedence (e.g. "1.0-beta" is newer than "1.0-unofficial"). + /// + /// This optionally also supports four-part versions, a non-standard extension used by Stardew Valley on ported platforms to represent platform-specific patches to a ported version, represented as a fourth number in the version string. /// public class SemanticVersion : ISemanticVersion { @@ -16,14 +19,7 @@ namespace StardewModdingAPI.Toolkit ** Fields *********/ /// A regex pattern matching a valid prerelease or build metadata tag. - internal const string TagPattern = @"(?>[a-z0-9]+[\-\.]?)+"; - - /// A regex pattern matching a version within a larger string. - internal const string UnboundedVersionPattern = @"(?>(?0|[1-9]\d*))\.(?>(?0|[1-9]\d*))(?>(?:\.(?0|[1-9]\d*))?)(?:-(?" + SemanticVersion.TagPattern + "))?(?:\\+(?" + SemanticVersion.TagPattern + "))?"; - - /// A regular expression matching a semantic version string. - /// This pattern is derived from the BNF documentation in the semver repo, with deviations to support the Stardew Valley mod conventions (see remarks on ). - internal static readonly Regex Regex = new Regex($@"^{SemanticVersion.UnboundedVersionPattern}$", RegexOptions.CultureInvariant | RegexOptions.IgnoreCase | RegexOptions.Compiled | RegexOptions.ExplicitCapture); + private const string TagPattern = @"(?>[a-z0-9]+[\-\.]?)+"; /********* @@ -38,6 +34,9 @@ namespace StardewModdingAPI.Toolkit /// The patch version for backwards-compatible bug fixes. public int PatchVersion { get; } + /// The platform release. This is a non-standard semver extension used by Stardew Valley on ported platforms to represent platform-specific patches to a ported version, represented as a fourth number in the version string. + public int PlatformRelease { get; } + /// An optional prerelease tag. public string PrereleaseTag { get; } @@ -52,13 +51,15 @@ namespace StardewModdingAPI.Toolkit /// The major version incremented for major API changes. /// The minor version incremented for backwards-compatible changes. /// The patch version for backwards-compatible fixes. + /// The platform-specific version (if applicable). /// An optional prerelease tag. /// Optional build metadata. This is ignored when determining version precedence. - public SemanticVersion(int major, int minor, int patch, string prereleaseTag = null, string buildMetadata = null) + public SemanticVersion(int major, int minor, int patch, int platformRelease = 0, string prereleaseTag = null, string buildMetadata = null) { this.MajorVersion = major; this.MinorVersion = minor; this.PatchVersion = patch; + this.PlatformRelease = platformRelease; this.PrereleaseTag = this.GetNormalizedTag(prereleaseTag); this.BuildMetadata = this.GetNormalizedTag(buildMetadata); @@ -82,23 +83,22 @@ namespace StardewModdingAPI.Toolkit /// Construct an instance. /// The semantic version string. + /// Whether to recognize non-standard semver extensions. /// The is null. /// The is not a valid semantic version. - public SemanticVersion(string version) + public SemanticVersion(string version, bool allowNonStandard = false) { - // parse if (version == null) throw new ArgumentNullException(nameof(version), "The input version string can't be null."); - var match = SemanticVersion.Regex.Match(version.Trim()); - if (!match.Success) + if (!SemanticVersionReader.TryParse(version, allowNonStandard, out int major, out int minor, out int patch, out int platformRelease, out string prereleaseTag, out string buildMetadata) || (!allowNonStandard && platformRelease != 0)) throw new FormatException($"The input '{version}' isn't a valid semantic version."); - // initialize - this.MajorVersion = int.Parse(match.Groups["major"].Value); - this.MinorVersion = match.Groups["minor"].Success ? int.Parse(match.Groups["minor"].Value) : 0; - this.PatchVersion = match.Groups["patch"].Success ? int.Parse(match.Groups["patch"].Value) : 0; - this.PrereleaseTag = match.Groups["prerelease"].Success ? this.GetNormalizedTag(match.Groups["prerelease"].Value) : null; - this.BuildMetadata = match.Groups["buildmetadata"].Success ? this.GetNormalizedTag(match.Groups["buildmetadata"].Value) : null; + this.MajorVersion = major; + this.MinorVersion = minor; + this.PatchVersion = patch; + this.PlatformRelease = platformRelease; + this.PrereleaseTag = prereleaseTag; + this.BuildMetadata = buildMetadata; this.AssertValid(); } @@ -110,7 +110,7 @@ namespace StardewModdingAPI.Toolkit { if (other == null) throw new ArgumentNullException(nameof(other)); - return this.CompareTo(other.MajorVersion, other.MinorVersion, other.PatchVersion, other.PrereleaseTag); + return this.CompareTo(other.MajorVersion, other.MinorVersion, other.PatchVersion, (other as SemanticVersion)?.PlatformRelease ?? 0, other.PrereleaseTag); } /// Indicates whether the current object is equal to another object of the same type. @@ -139,7 +139,7 @@ namespace StardewModdingAPI.Toolkit /// The specified version is not a valid semantic version. public bool IsOlderThan(string other) { - return this.IsOlderThan(new SemanticVersion(other)); + return this.IsOlderThan(new SemanticVersion(other, allowNonStandard: true)); } /// Get whether this version is newer than the specified version. @@ -154,7 +154,7 @@ namespace StardewModdingAPI.Toolkit /// The specified version is not a valid semantic version. public bool IsNewerThan(string other) { - return this.IsNewerThan(new SemanticVersion(other)); + return this.IsNewerThan(new SemanticVersion(other, allowNonStandard: true)); } /// Get whether this version is between two specified versions (inclusively). @@ -171,13 +171,15 @@ namespace StardewModdingAPI.Toolkit /// One of the specified versions is not a valid semantic version. public bool IsBetween(string min, string max) { - return this.IsBetween(new SemanticVersion(min), new SemanticVersion(max)); + return this.IsBetween(new SemanticVersion(min, allowNonStandard: true), new SemanticVersion(max, allowNonStandard: true)); } /// Get a string representation of the version. public override string ToString() { string version = $"{this.MajorVersion}.{this.MinorVersion}.{this.PatchVersion}"; + if (this.PlatformRelease != 0) + version += $".{this.PlatformRelease}"; if (this.PrereleaseTag != null) version += $"-{this.PrereleaseTag}"; if (this.BuildMetadata != null) @@ -185,15 +187,30 @@ namespace StardewModdingAPI.Toolkit return version; } + /// Whether the version uses non-standard extensions, like four-part game versions on some platforms. + public bool IsNonStandard() + { + return this.PlatformRelease != 0; + } + /// Parse a version string without throwing an exception if it fails. /// The version string. /// The parsed representation. /// Returns whether parsing the version succeeded. public static bool TryParse(string version, out ISemanticVersion parsed) + { + return SemanticVersion.TryParseNonStandard(version, out parsed) && !parsed.IsNonStandard(); + } + + /// Parse a version string without throwing an exception if it fails, including support for non-standard extensions like . + /// The version string. + /// The parsed representation. + /// Returns whether parsing the version succeeded. + public static bool TryParseNonStandard(string version, out ISemanticVersion parsed) { try { - parsed = new SemanticVersion(version); + parsed = new SemanticVersion(version, true); return true; } catch @@ -219,8 +236,9 @@ namespace StardewModdingAPI.Toolkit /// The major version to compare with this instance. /// The minor version to compare with this instance. /// The patch version to compare with this instance. + /// The non-standard platform release to compare with this instance. /// The prerelease tag to compare with this instance. - private int CompareTo(int otherMajor, int otherMinor, int otherPatch, string otherTag) + private int CompareTo(int otherMajor, int otherMinor, int otherPatch, int otherPlatformRelease, string otherTag) { const int same = 0; const int curNewer = 1; @@ -233,6 +251,8 @@ namespace StardewModdingAPI.Toolkit return this.MinorVersion.CompareTo(otherMinor); if (this.PatchVersion != otherPatch) return this.PatchVersion.CompareTo(otherPatch); + if (this.PlatformRelease != otherPlatformRelease) + return this.PlatformRelease.CompareTo(otherPlatformRelease); if (this.PrereleaseTag == otherTag) return same; @@ -274,7 +294,7 @@ namespace StardewModdingAPI.Toolkit } // fallback (this should never happen) - return string.Compare(this.ToString(), new SemanticVersion(otherMajor, otherMinor, otherPatch, otherTag).ToString(), StringComparison.InvariantCultureIgnoreCase); + return string.Compare(this.ToString(), new SemanticVersion(otherMajor, otherMinor, otherPatch, otherPlatformRelease, otherTag).ToString(), StringComparison.InvariantCultureIgnoreCase); } /// Assert that the current version is valid. diff --git a/src/SMAPI.Toolkit/Serialization/Converters/SemanticVersionConverter.cs b/src/SMAPI.Toolkit/Serialization/Converters/SemanticVersionConverter.cs index ece4a72e..e1b9db1d 100644 --- a/src/SMAPI.Toolkit/Serialization/Converters/SemanticVersionConverter.cs +++ b/src/SMAPI.Toolkit/Serialization/Converters/SemanticVersionConverter.cs @@ -68,7 +68,7 @@ namespace StardewModdingAPI.Toolkit.Serialization.Converters int patch = obj.ValueIgnoreCase(nameof(ISemanticVersion.PatchVersion)); string prereleaseTag = obj.ValueIgnoreCase(nameof(ISemanticVersion.PrereleaseTag)); - return new SemanticVersion(major, minor, patch, prereleaseTag); + return new SemanticVersion(major, minor, patch, prereleaseTag: prereleaseTag); } /// Read a JSON string. diff --git a/src/SMAPI.Web/Framework/LogParsing/LogParser.cs b/src/SMAPI.Web/Framework/LogParsing/LogParser.cs index 1210f708..cc91ec51 100644 --- a/src/SMAPI.Web/Framework/LogParsing/LogParser.cs +++ b/src/SMAPI.Web/Framework/LogParsing/LogParser.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.IO; using System.Linq; using System.Text.RegularExpressions; -using StardewModdingAPI.Toolkit; using StardewModdingAPI.Web.Framework.LogParsing.Models; namespace StardewModdingAPI.Web.Framework.LogParsing @@ -31,22 +30,22 @@ namespace StardewModdingAPI.Web.Framework.LogParsing /// A regex pattern matching an entry in SMAPI's mod list. /// The author name and description are optional. - private readonly Regex ModListEntryPattern = new Regex(@"^ (?.+?) (?" + SemanticVersion.UnboundedVersionPattern + @")(?: by (?[^\|]+))?(?: \| (?.+))?$", RegexOptions.Compiled | RegexOptions.IgnoreCase); + private readonly Regex ModListEntryPattern = new Regex(@"^ (?.+?) (?[^\s]+)(?: by (?[^\|]+))?(?: \| (?.+))?$", RegexOptions.Compiled | RegexOptions.IgnoreCase); /// A regex pattern matching the start of SMAPI's content pack list. private readonly Regex ContentPackListStartPattern = new Regex(@"^Loaded \d+ content packs:$", RegexOptions.Compiled | RegexOptions.IgnoreCase); /// A regex pattern matching an entry in SMAPI's content pack list. - private readonly Regex ContentPackListEntryPattern = new Regex(@"^ (?.+?) (?" + SemanticVersion.UnboundedVersionPattern + @")(?: by (?[^\|]+))? \| for (?[^\|]+)(?: \| (?.+))?$", RegexOptions.Compiled | RegexOptions.IgnoreCase); + private readonly Regex ContentPackListEntryPattern = new Regex(@"^ (?.+?) (?[^\s]+)(?: by (?[^\|]+))? \| for (?[^\|]+)(?: \| (?.+))?$", RegexOptions.Compiled | RegexOptions.IgnoreCase); /// A regex pattern matching the start of SMAPI's mod update list. private readonly Regex ModUpdateListStartPattern = new Regex(@"^You can update \d+ mods?:$", RegexOptions.Compiled | RegexOptions.IgnoreCase); /// A regex pattern matching an entry in SMAPI's mod update list. - private readonly Regex ModUpdateListEntryPattern = new Regex(@"^ (?.+?) (?" + SemanticVersion.UnboundedVersionPattern + @"): (?.+)$", RegexOptions.Compiled | RegexOptions.IgnoreCase); + private readonly Regex ModUpdateListEntryPattern = new Regex(@"^ (?.+?) (?[^\s]+): (?.+)$", RegexOptions.Compiled | RegexOptions.IgnoreCase); /// A regex pattern matching SMAPI's update line. - private readonly Regex SMAPIUpdatePattern = new Regex(@"^You can update SMAPI to (?" + SemanticVersion.UnboundedVersionPattern + @"): (?.+)$", RegexOptions.Compiled | RegexOptions.IgnoreCase); + private readonly Regex SMAPIUpdatePattern = new Regex(@"^You can update SMAPI to (?[^\s]+): (?.+)$", RegexOptions.Compiled | RegexOptions.IgnoreCase); /********* diff --git a/src/SMAPI.Web/Framework/VersionConstraint.cs b/src/SMAPI.Web/Framework/VersionConstraint.cs index 2d6ec603..72f5ef84 100644 --- a/src/SMAPI.Web/Framework/VersionConstraint.cs +++ b/src/SMAPI.Web/Framework/VersionConstraint.cs @@ -1,16 +1,34 @@ -using Microsoft.AspNetCore.Routing.Constraints; +using System; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Routing; using StardewModdingAPI.Toolkit; namespace StardewModdingAPI.Web.Framework { /// Constrains a route value to a valid semantic version. - internal class VersionConstraint : RegexRouteConstraint + internal class VersionConstraint : IRouteConstraint { /********* ** Public methods *********/ - /// Construct an instance. - public VersionConstraint() - : base(SemanticVersion.Regex) { } + /// Get whether the URL parameter contains a valid value for this constraint. + /// An object that encapsulates information about the HTTP request. + /// The router that this constraint belongs to. + /// The name of the parameter that is being checked. + /// A dictionary that contains the parameters for the URL. + /// An object that indicates whether the constraint check is being performed when an incoming request is being handled or when a URL is being generated. + /// true if the URL parameter contains a valid value; otherwise, false. + public bool Match(HttpContext httpContext, IRouter route, string routeKey, RouteValueDictionary values, RouteDirection routeDirection) + { + if (routeKey == null) + throw new ArgumentNullException(nameof(routeKey)); + if (values == null) + throw new ArgumentNullException(nameof(values)); + + return + values.TryGetValue(routeKey, out object routeValue) + && routeValue is string routeStr + && SemanticVersion.TryParseNonStandard(routeStr, out _); + } } } diff --git a/src/SMAPI/Framework/GameVersion.cs b/src/SMAPI/Framework/GameVersion.cs index 29cfbc39..07957624 100644 --- a/src/SMAPI/Framework/GameVersion.cs +++ b/src/SMAPI/Framework/GameVersion.cs @@ -3,8 +3,8 @@ using System.Collections.Generic; namespace StardewModdingAPI.Framework { - /// An implementation of that correctly handles the non-semantic versions used by older Stardew Valley releases. - internal class GameVersion : SemanticVersion + /// An extension of that correctly handles non-semantic versions used by Stardew Valley. + internal class GameVersion : Toolkit.SemanticVersion { /********* ** Private methods @@ -18,11 +18,11 @@ namespace StardewModdingAPI.Framework ["1.03"] = "1.0.3", ["1.04"] = "1.0.4", ["1.05"] = "1.0.5", - ["1.051"] = "1.0.6-prerelease1", // not a very good mapping, but good enough for SMAPI's purposes. - ["1.051b"] = "1.0.6-prerelease2", + ["1.051"] = "1.0.5.1", + ["1.051b"] = "1.0.5.2", ["1.06"] = "1.0.6", ["1.07"] = "1.0.7", - ["1.07a"] = "1.0.8-prerelease1", + ["1.07a"] = "1.0.7.1", ["1.08"] = "1.0.8", ["1.1"] = "1.1.0", ["1.2"] = "1.2.0", @@ -36,7 +36,7 @@ namespace StardewModdingAPI.Framework /// Construct an instance. /// The game version string. public GameVersion(string version) - : base(GameVersion.GetSemanticVersionString(version)) { } + : base(GameVersion.GetSemanticVersionString(version), allowNonStandard: true) { } /// Get a string representation of the version. public override string ToString() @@ -53,33 +53,21 @@ namespace StardewModdingAPI.Framework private static string GetSemanticVersionString(string gameVersion) { // mapped version - if (GameVersion.VersionMap.TryGetValue(gameVersion, out string semanticVersion)) - return semanticVersion; - - // special case: four-part versions - string[] parts = gameVersion.Split('.'); - if (parts.Length == 4) - return $"{parts[0]}.{parts[1]}.{parts[2]}+{parts[3]}"; - - return gameVersion; + return GameVersion.VersionMap.TryGetValue(gameVersion, out string semanticVersion) + ? semanticVersion + : gameVersion; } /// Convert a semantic version string to the equivalent game version string. /// The semantic version string. private static string GetGameVersionString(string semanticVersion) { - // mapped versions foreach (var mapping in GameVersion.VersionMap) { if (mapping.Value.Equals(semanticVersion, StringComparison.InvariantCultureIgnoreCase)) return mapping.Key; } - // special case: four-part versions - string[] parts = semanticVersion.Split('.', '+'); - if (parts.Length == 4) - return $"{parts[0]}.{parts[1]}.{parts[2]}.{parts[3]}"; - return semanticVersion; } } diff --git a/src/SMAPI/SemanticVersion.cs b/src/SMAPI/SemanticVersion.cs index 2a33ecef..4a175efe 100644 --- a/src/SMAPI/SemanticVersion.cs +++ b/src/SMAPI/SemanticVersion.cs @@ -39,18 +39,36 @@ namespace StardewModdingAPI /// The major version incremented for major API changes. /// The minor version incremented for backwards-compatible changes. /// The patch version for backwards-compatible bug fixes. - /// An optional prerelease tag. - /// Optional build metadata. This is ignored when determining version precedence. + /// An optional prerelease tag. + /// Optional build metadata. This is ignored when determining version precedence. + public SemanticVersion(int majorVersion, int minorVersion, int patchVersion, string prereleaseTag = null, string buildMetadata = null) + : this(majorVersion, minorVersion, patchVersion, 0, prereleaseTag, buildMetadata) { } + + /// Construct an instance. + /// The major version incremented for major API changes. + /// The minor version incremented for backwards-compatible changes. + /// The patch version for backwards-compatible bug fixes. + /// An optional prerelease tag. + /// The platform-specific version (if applicable). + /// Optional build metadata. This is ignored when determining version precedence. [JsonConstructor] - public SemanticVersion(int majorVersion, int minorVersion, int patchVersion, string prerelease = null, string build = null) - : this(new Toolkit.SemanticVersion(majorVersion, minorVersion, patchVersion, prerelease, build)) { } + internal SemanticVersion(int majorVersion, int minorVersion, int patchVersion, int platformRelease, string prereleaseTag = null, string buildMetadata = null) + : this(new Toolkit.SemanticVersion(majorVersion, minorVersion, patchVersion, platformRelease, prereleaseTag, buildMetadata)) { } /// Construct an instance. /// The semantic version string. /// The is null. /// The is not a valid semantic version. public SemanticVersion(string version) - : this(new Toolkit.SemanticVersion(version)) { } + : this(version, allowNonStandard: false) { } + + /// Construct an instance. + /// The semantic version string. + /// Whether to recognize non-standard semver extensions. + /// The is null. + /// The is not a valid semantic version. + internal SemanticVersion(string version, bool allowNonStandard) + : this(new Toolkit.SemanticVersion(version, allowNonStandard)) { } /// Construct an instance. /// The assembly version. @@ -141,6 +159,12 @@ namespace StardewModdingAPI return this.Version.ToString(); } + /// Whether the version uses non-standard extensions, like four-part game versions on some platforms. + public bool IsNonStandard() + { + return this.Version.IsNonStandard(); + } + /// Parse a version string without throwing an exception if it fails. /// The version string. /// The parsed representation. -- cgit From e33386abcc03c6fd94c365309a23e66f0fe9d6eb Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 25 Jan 2020 11:00:37 -0500 Subject: prevent load crashes due to invalid building types --- docs/release-notes.md | 1 + src/SMAPI/Patches/LoadErrorPatch.cs | 87 +++++++++++++++++++++++++++++-------- 2 files changed, 71 insertions(+), 17 deletions(-) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index 35fdbb4b..970f26f1 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -5,6 +5,7 @@ * For players: * SMAPI now prevents mods from crashing the game with invalid schedule data. + * SMAPI now prevents load crashes due to invalid building types. * Updated minimum game version (1.4 → 1.4.1). * Fixed 'collection was modified' error when returning to title in rare cases. * Fixed update-check error if a mod's Chucklefish page has no version. diff --git a/src/SMAPI/Patches/LoadErrorPatch.cs b/src/SMAPI/Patches/LoadErrorPatch.cs index eedb4164..c16ca7cc 100644 --- a/src/SMAPI/Patches/LoadErrorPatch.cs +++ b/src/SMAPI/Patches/LoadErrorPatch.cs @@ -3,8 +3,10 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Linq; using Harmony; +using StardewModdingAPI.Framework.Exceptions; using StardewModdingAPI.Framework.Patching; using StardewValley; +using StardewValley.Buildings; using StardewValley.Locations; namespace StardewModdingAPI.Patches @@ -63,11 +65,25 @@ namespace StardewModdingAPI.Patches /// The game locations being loaded. /// Returns whether to execute the original method. private static bool Before_SaveGame_LoadDataToLocations(List gamelocations) + { + bool removedAny = + LoadErrorPatch.RemoveInvalidLocations(gamelocations) + | LoadErrorPatch.RemoveBrokenBuildings(gamelocations) + | LoadErrorPatch.RemoveInvalidNpcs(gamelocations); + + if (removedAny) + LoadErrorPatch.OnContentRemoved(); + + return true; + } + + /// Remove locations which don't exist in-game. + /// The current game locations. + private static bool RemoveInvalidLocations(List locations) { bool removedAny = false; - // remove invalid locations - foreach (GameLocation location in gamelocations.ToArray()) + foreach (GameLocation location in locations.ToArray()) { if (location is Cellar) continue; // missing cellars will be added by the game code @@ -75,23 +91,48 @@ namespace StardewModdingAPI.Patches if (Game1.getLocationFromName(location.name) == null) { LoadErrorPatch.Monitor.Log($"Removed invalid location '{location.Name}' to avoid a crash when loading save '{Constants.SaveFolderName}'. (Did you remove a custom location mod?)", LogLevel.Warn); - gamelocations.Remove(location); + locations.Remove(location); removedAny = true; } } - // get building interiors - var interiors = - ( - from location in gamelocations.OfType() - from building in location.buildings - where building.indoors.Value != null - select building.indoors.Value - ); + return removedAny; + } + + /// Remove buildings which don't exist in the game data. + /// The current game locations. + private static bool RemoveBrokenBuildings(IEnumerable locations) + { + bool removedAny = false; + + foreach (BuildableGameLocation location in locations.OfType()) + { + foreach (Building building in location.buildings.ToArray()) + { + try + { + BluePrint _ = new BluePrint(building.buildingType.Value); + } + catch (SContentLoadException) + { + LoadErrorPatch.Monitor.Log($"Removed invalid building type '{building.buildingType.Value}' in {location.Name} ({building.tileX}, {building.tileY}) to avoid a crash when loading save '{Constants.SaveFolderName}'. (Did you remove a custom building mod?)", LogLevel.Warn); + location.buildings.Remove(building); + removedAny = true; + } + } + } + + return removedAny; + } + + /// Remove NPCs which don't exist in the game data. + /// The current game locations. + private static bool RemoveInvalidNpcs(IEnumerable locations) + { + bool removedAny = false; - // remove custom NPCs which no longer exist IDictionary data = Game1.content.Load>("Data\\NPCDispositions"); - foreach (GameLocation location in gamelocations.Concat(interiors)) + foreach (GameLocation location in LoadErrorPatch.GetAllLocations(locations)) { foreach (NPC npc in location.characters.ToArray()) { @@ -103,7 +144,7 @@ namespace StardewModdingAPI.Patches } catch { - LoadErrorPatch.Monitor.Log($"Removed invalid villager '{npc.Name}' to avoid a crash when loading save '{Constants.SaveFolderName}'. (Did you remove a custom NPC mod?)", LogLevel.Warn); + LoadErrorPatch.Monitor.Log($"Removed invalid villager '{npc.Name}' in {location.Name} ({npc.getTileLocation()}) to avoid a crash when loading save '{Constants.SaveFolderName}'. (Did you remove a custom NPC mod?)", LogLevel.Warn); location.characters.Remove(npc); removedAny = true; } @@ -111,10 +152,22 @@ namespace StardewModdingAPI.Patches } } - if (removedAny) - LoadErrorPatch.OnContentRemoved(); + return removedAny; + } - return true; + /// Get all locations, including building interiors. + /// The main game locations. + private static IEnumerable GetAllLocations(IEnumerable locations) + { + foreach (GameLocation location in locations) + { + yield return location; + if (location is BuildableGameLocation buildableLocation) + { + foreach (GameLocation interior in buildableLocation.buildings.Select(p => p.indoors.Value).Where(p => p != null)) + yield return interior; + } + } } } } -- cgit From 4db7ca28f68be2e25b565a1b98e2c05fb42a5a88 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 25 Jan 2020 11:05:36 -0500 Subject: fix error building/demolishing buildings for some players --- docs/release-notes.md | 1 + src/SMAPI/Framework/SnapshotListDiff.cs | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index 970f26f1..d8ff8f6a 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -10,6 +10,7 @@ * Fixed 'collection was modified' error when returning to title in rare cases. * Fixed update-check error if a mod's Chucklefish page has no version. * Fixed SMAPI beta versions not showing update alert on next launch (thanks to danvolchek!). + * Fixed rare error when building/demolishing buildings. * For the Console Commands mod: * Added `test_input` command to view button codes in the console. diff --git a/src/SMAPI/Framework/SnapshotListDiff.cs b/src/SMAPI/Framework/SnapshotListDiff.cs index d4d5df50..2d0efa0d 100644 --- a/src/SMAPI/Framework/SnapshotListDiff.cs +++ b/src/SMAPI/Framework/SnapshotListDiff.cs @@ -42,10 +42,12 @@ namespace StardewModdingAPI.Framework this.IsChanged = isChanged; this.RemovedImpl.Clear(); - this.RemovedImpl.AddRange(removed); + if (removed != null) + this.RemovedImpl.AddRange(removed); this.AddedImpl.Clear(); - this.AddedImpl.AddRange(added); + if (added != null) + this.AddedImpl.AddRange(added); } /// Update the snapshot. -- cgit From 04d3f9b5899f0e7c0fe4d9c8247ba209c710bf2e Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 25 Jan 2020 15:19:47 -0500 Subject: add internal method for Content Patcher --- src/SMAPI/Framework/Content/ContentCache.cs | 1 - src/SMAPI/Framework/ContentCoordinator.cs | 18 ++++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/SMAPI/Framework/Content/ContentCache.cs b/src/SMAPI/Framework/Content/ContentCache.cs index f33ff84d..b0933ac6 100644 --- a/src/SMAPI/Framework/Content/ContentCache.cs +++ b/src/SMAPI/Framework/Content/ContentCache.cs @@ -4,7 +4,6 @@ using System.Diagnostics.Contracts; using System.Linq; using Microsoft.Xna.Framework; using StardewModdingAPI.Framework.Reflection; -using StardewModdingAPI.Internal; using StardewModdingAPI.Toolkit.Utilities; using StardewValley; diff --git a/src/SMAPI/Framework/ContentCoordinator.cs b/src/SMAPI/Framework/ContentCoordinator.cs index b2bacea6..b60483f1 100644 --- a/src/SMAPI/Framework/ContentCoordinator.cs +++ b/src/SMAPI/Framework/ContentCoordinator.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO; using System.Linq; @@ -251,6 +252,23 @@ namespace StardewModdingAPI.Framework return removedAssets.Keys; } + /// Get all loaded instances of an asset name. + /// The asset name. + [SuppressMessage("ReSharper", "UnusedMember.Global", Justification = "This method is provided for Content Patcher.")] + public IEnumerable GetLoadedValues(string assetName) + { + return this.ContentManagerLock.InReadLock(() => + { + List values = new List(); + foreach (IContentManager content in this.ContentManagers.Where(p => !p.IsNamespaced && p.IsLoaded(assetName))) + { + object value = content.Load(assetName, this.Language, useCache: true); + values.Add(value); + } + return values; + }); + } + /// Dispose held resources. public void Dispose() { -- cgit From 7a6dab7548e7bc32a685e916edec83f6458881c1 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 25 Jan 2020 15:21:40 -0500 Subject: fix dialogue asset propagation --- docs/release-notes.md | 1 + src/SMAPI/Metadata/CoreAssetPropagator.cs | 4 ++++ 2 files changed, 5 insertions(+) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index d8ff8f6a..fdae4dc5 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -19,6 +19,7 @@ * Asset propagation for player sprites now affects other players' sprites, and updates recolor maps (e.g. sleeves). * Removed invalid-schedule validation which had false positives. * Fixed `helper.Data.Read/WriteGlobalData` using the `Saves` folder. The installer will move existing folders to the appdata folder. + * Fixed dialogue asset changes not correctly propagated until the next day. * For SMAPI/tool developers: * Added internal support for four-part versions to support SMAPI on Android. diff --git a/src/SMAPI/Metadata/CoreAssetPropagator.cs b/src/SMAPI/Metadata/CoreAssetPropagator.cs index 57e1d197..7a58d52c 100644 --- a/src/SMAPI/Metadata/CoreAssetPropagator.cs +++ b/src/SMAPI/Metadata/CoreAssetPropagator.cs @@ -887,7 +887,11 @@ namespace StardewModdingAPI.Metadata // update dialogue foreach (NPC villager in villagers) + { villager.resetSeasonalDialogue(); // doesn't only affect seasonal dialogue + villager.resetCurrentDialogue(); + } + return true; } -- cgit From fc0b98be4584eec2b06a3af09cc4b85f9e9a9efc Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 25 Jan 2020 18:22:50 -0500 Subject: add user settings that override defaults (#693) --- docs/release-notes.md | 1 + docs/technical/smapi.md | 13 ++----------- src/SMAPI/Constants.cs | 3 +++ src/SMAPI/Framework/SCore.cs | 3 +++ src/SMAPI/SMAPI.config.json | 6 ++++++ 5 files changed, 15 insertions(+), 11 deletions(-) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index fdae4dc5..dc26db2d 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -6,6 +6,7 @@ * For players: * SMAPI now prevents mods from crashing the game with invalid schedule data. * SMAPI now prevents load crashes due to invalid building types. + * Added support for persistent `smapi-internal/config.json` overrides (see info in the file). * Updated minimum game version (1.4 → 1.4.1). * Fixed 'collection was modified' error when returning to title in rare cases. * Fixed update-check error if a mod's Chucklefish page has no version. diff --git a/docs/technical/smapi.md b/docs/technical/smapi.md index d565aeb4..c9d5c07e 100644 --- a/docs/technical/smapi.md +++ b/docs/technical/smapi.md @@ -19,17 +19,8 @@ This document is about SMAPI itself; see also [mod build package](mod-package.md ## Customisation ### Configuration file -You can customise the SMAPI behaviour by editing the `smapi-internal/config.json` file in your game -folder. - -Basic fields: - -field | purpose ------------------ | ------- -`DeveloperMode` | Default `false` (except in _SMAPI for developers_ releases). Whether to enable features intended for mod developers (mainly more detailed console logging). -`CheckForUpdates` | Default `true`. Whether SMAPI should check for a newer version when you load the game. If a new version is available, a small message will appear in the console. This doesn't affect the load time even if your connection is offline or slow, because it happens in the background. -`VerboseLogging` | Default `false`. Whether SMAPI should log more information about the game context. -`ModData` | Internal metadata about SMAPI mods. Changing this isn't recommended and may destabilise your game. See documentation in the file. +You can customise some SMAPI behaviour by editing the `smapi-internal/config.json` file in your +game folder. See documentation in the file for more info. ### Command-line arguments The SMAPI installer recognises three command-line arguments: diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index da2ee375..d2af5de2 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -61,6 +61,9 @@ namespace StardewModdingAPI /// The file path for the SMAPI configuration file. internal static string ApiConfigPath => Path.Combine(Constants.InternalFilesPath, "config.json"); + /// The file path for the overrides file for , which is applied over it. + internal static string ApiUserConfigPath => Path.Combine(Constants.InternalFilesPath, "config.user.json"); + /// The file path for the SMAPI metadata file. internal static string ApiMetadataPath => Path.Combine(Constants.InternalFilesPath, "metadata.json"); diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index d71b5e5a..81b7c2e8 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -153,6 +153,9 @@ namespace StardewModdingAPI.Framework // init basics this.Settings = JsonConvert.DeserializeObject(File.ReadAllText(Constants.ApiConfigPath)); + if (File.Exists(Constants.ApiUserConfigPath)) + JsonConvert.PopulateObject(File.ReadAllText(Constants.ApiUserConfigPath), this.Settings); + this.LogFile = new LogFileManager(logPath); this.Monitor = new Monitor("SMAPI", this.ConsoleManager, this.LogFile, this.Settings.ConsoleColors, this.Settings.VerboseLogging) { diff --git a/src/SMAPI/SMAPI.config.json b/src/SMAPI/SMAPI.config.json index 824bb783..57b4f885 100644 --- a/src/SMAPI/SMAPI.config.json +++ b/src/SMAPI/SMAPI.config.json @@ -6,6 +6,12 @@ This file contains advanced configuration for SMAPI. You generally shouldn't cha The default values are mirrored in StardewModdingAPI.Framework.Models.SConfig to log custom changes. +This file is overwritten each time you update or reinstall SMAPI. To avoid losing custom settings, +create a 'config.user.json' file in the same folder with *only* the settings you want to change. +That file won't be overwritten, and any settings in it will override the default options. Don't +copy all the settings, or you may cause bugs due to overridden changes in future SMAPI versions. + + */ { -- cgit From a96bfea205ab855913726cad58d63191fbf24399 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 25 Jan 2020 18:37:17 -0500 Subject: back up config.user.json when installing (#693) --- src/SMAPI.Installer/Framework/InstallerPaths.cs | 18 ++++++++++++++++++ src/SMAPI.Installer/InteractiveInstaller.cs | 6 ++++++ 2 files changed, 24 insertions(+) (limited to 'src') diff --git a/src/SMAPI.Installer/Framework/InstallerPaths.cs b/src/SMAPI.Installer/Framework/InstallerPaths.cs index 9393e14f..ac6c3a8e 100644 --- a/src/SMAPI.Installer/Framework/InstallerPaths.cs +++ b/src/SMAPI.Installer/Framework/InstallerPaths.cs @@ -8,6 +8,9 @@ namespace StardewModdingAPI.Installer.Framework /********* ** Accessors *********/ + /**** + ** Main folders + ****/ /// The directory path containing the files to copy into the game folder. public DirectoryInfo BundleDir { get; } @@ -17,9 +20,18 @@ namespace StardewModdingAPI.Installer.Framework /// The directory into which to install mods. public DirectoryInfo ModsDir { get; } + /**** + ** Installer paths + ****/ /// The full path to directory path containing the files to copy into the game folder. public string BundlePath => this.BundleDir.FullName; + /// The full path to the backup API user settings folder, if applicable. + public string BundleApiUserConfigPath { get; } + + /**** + ** Game paths + ****/ /// The full path to the directory containing the installed game. public string GamePath => this.GameDir.FullName; @@ -29,6 +41,9 @@ namespace StardewModdingAPI.Installer.Framework /// The full path to SMAPI's internal configuration file. public string ApiConfigPath { get; } + /// The full path to the user's config overrides file. + public string ApiUserConfigPath { get; } + /// The full path to the installed SMAPI executable file. public string ExecutablePath { get; } @@ -55,11 +70,14 @@ namespace StardewModdingAPI.Installer.Framework this.GameDir = gameDir; this.ModsDir = new DirectoryInfo(Path.Combine(gameDir.FullName, "Mods")); + this.BundleApiUserConfigPath = Path.Combine(bundleDir.FullName, "smapi-internal", "config.user.json"); + this.ExecutablePath = Path.Combine(gameDir.FullName, gameExecutableName); this.UnixLauncherPath = Path.Combine(gameDir.FullName, "StardewValley"); this.UnixSmapiLauncherPath = Path.Combine(gameDir.FullName, "StardewModdingAPI"); this.UnixBackupLauncherPath = Path.Combine(gameDir.FullName, "StardewValley-original"); this.ApiConfigPath = Path.Combine(gameDir.FullName, "smapi-internal", "config.json"); + this.ApiUserConfigPath = Path.Combine(gameDir.FullName, "smapi-internal", "config.user.json"); } } } diff --git a/src/SMAPI.Installer/InteractiveInstaller.cs b/src/SMAPI.Installer/InteractiveInstaller.cs index 14f37258..2d58baf0 100644 --- a/src/SMAPI.Installer/InteractiveInstaller.cs +++ b/src/SMAPI.Installer/InteractiveInstaller.cs @@ -351,6 +351,12 @@ namespace StardewModdingApi.Installer this.PrintDebug("----------------------------------------------------------------------------"); Console.WriteLine(); + /**** + ** Back up user settings + ****/ + if (File.Exists(paths.ApiUserConfigPath)) + File.Copy(paths.ApiUserConfigPath, paths.BundleApiUserConfigPath); + /**** ** Always uninstall old files ****/ -- cgit From 22a0a32b6d959946bfd80bf0ca9796378f36e0cd Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 26 Jan 2020 19:49:17 -0500 Subject: refactor performance counter code This commit performs some general refactoring, including... - avoid manually duplicating the event list; - rework the 'is important' event flag; - remove the new packages (Cyotek.Collections can be replaced with built-in types, and System.ValueTuple won't work in the Mono version used on Linux/Mac); - improve performance; - minor cleanup. --- build/common.targets | 1 - build/prepare-install-package.targets | 1 - .../Framework/Commands/ArgumentParser.cs | 22 +- .../Commands/Other/PerformanceCounterCommand.cs | 655 ++++++++++----------- .../Framework/Commands/TrainerCommand.cs | 18 +- .../SMAPI.Mods.ConsoleCommands.csproj | 4 - src/SMAPI/Constants.cs | 3 +- src/SMAPI/Framework/Events/EventManager.cs | 48 +- src/SMAPI/Framework/Events/IManagedEvent.cs | 10 +- src/SMAPI/Framework/Events/ManagedEvent.cs | 71 +-- .../Framework/PerformanceCounter/AlertContext.cs | 14 +- .../Framework/PerformanceCounter/AlertEntry.cs | 31 +- .../EventPerformanceCounterCollection.cs | 16 - .../Framework/PerformanceCounter/PeakEntry.cs | 25 +- .../PerformanceCounter/PerformanceCounter.cs | 143 ++--- .../PerformanceCounterCollection.cs | 196 +++--- .../PerformanceCounter/PerformanceCounterEntry.cs | 26 +- .../PerformanceCounterManager.cs | 212 +++---- src/SMAPI/Framework/SCore.cs | 15 +- src/SMAPI/Framework/SGame.cs | 11 +- src/SMAPI/Program.cs | 2 +- src/SMAPI/SMAPI.csproj | 5 - 22 files changed, 706 insertions(+), 823 deletions(-) delete mode 100644 src/SMAPI/Framework/PerformanceCounter/EventPerformanceCounterCollection.cs (limited to 'src') diff --git a/build/common.targets b/build/common.targets index 78b435d0..df2d4861 100644 --- a/build/common.targets +++ b/build/common.targets @@ -32,7 +32,6 @@ - diff --git a/build/prepare-install-package.targets b/build/prepare-install-package.targets index 96716ecb..61b12039 100644 --- a/build/prepare-install-package.targets +++ b/build/prepare-install-package.targets @@ -41,7 +41,6 @@ - diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/ArgumentParser.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/ArgumentParser.cs index 40691a3e..9c7082c9 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/ArgumentParser.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/ArgumentParser.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections; using System.Collections.Generic; using System.Globalization; @@ -32,13 +32,6 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands /// The zero-based index of the element to get. public string this[int index] => this.Args[index]; - /// A method which parses a string argument into the given value. - /// The expected argument type. - /// The argument to parse. - /// The parsed value. - /// Returns whether the argument was successfully parsed. - public delegate bool ParseDelegate(string input, out T output); - /********* ** Public methods @@ -114,19 +107,6 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands return true; } - public bool IsDecimal(int index) - { - if (!this.TryGet(index, "", out string raw, false)) - return false; - - if (!decimal.TryParse(raw, NumberStyles.Number, CultureInfo.InvariantCulture, out decimal value)) - { - return false; - } - - return true; - } - /// Try to read a decimal argument. /// The argument index. /// The argument name for error messages. diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/PerformanceCounterCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/PerformanceCounterCommand.cs index 2260296b..820f1939 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/PerformanceCounterCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/PerformanceCounterCommand.cs @@ -7,24 +7,13 @@ using StardewModdingAPI.Framework.PerformanceCounter; namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other { - // ReSharper disable once UnusedType.Global + /// A set of commands which displays or configures performance monitoring. internal class PerformanceCounterCommand : TrainerCommand { - /// The command names and aliases - private readonly Dictionary SubCommandNames = new Dictionary() - { - {SubCommand.Summary, new[] {"summary", "sum", "s"}}, - {SubCommand.Detail, new[] {"detail", "d"}}, - {SubCommand.Reset, new[] {"reset", "r"}}, - {SubCommand.Trigger, new[] {"trigger"}}, - {SubCommand.Enable, new[] {"enable"}}, - {SubCommand.Disable, new[] {"disable"}}, - {SubCommand.Examples, new[] {"examples"}}, - {SubCommand.Concepts, new[] {"concepts"}}, - {SubCommand.Help, new[] {"help"}}, - }; - - /// The available commands enum + /********* + ** Fields + *********/ + /// The available commands. private enum SubCommand { Summary, @@ -33,16 +22,16 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other Trigger, Enable, Disable, - Examples, - Help, - Concepts, - None + Help } + + /********* + ** Public methods + *********/ /// Construct an instance. - public PerformanceCounterCommand() : base("pc", PerformanceCounterCommand.GetDescription()) - { - } + public PerformanceCounterCommand() + : base("performance", PerformanceCounterCommand.GetDescription()) { } /// Handle the command. /// Writes messages to the console and log file. @@ -50,92 +39,94 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other /// The command arguments. public override void Handle(IMonitor monitor, string command, ArgumentParser args) { - if (args.TryGet(0, "command", out string subCommandString, false)) + // parse args + SubCommand subcommand = SubCommand.Summary; { - SubCommand subSubCommand = this.ParseCommandString(subCommandString); - - switch (subSubCommand) + if (args.TryGet(0, "command", out string subcommandStr, false) && !Enum.TryParse(subcommandStr, ignoreCase: true, out subcommand)) { - case SubCommand.Summary: - this.HandleSummarySubCommand(monitor, args); - break; - case SubCommand.Detail: - this.HandleDetailSubCommand(monitor, args); - break; - case SubCommand.Reset: - this.HandleResetSubCommand(monitor, args); - break; - case SubCommand.Trigger: - this.HandleTriggerSubCommand(monitor, args); - break; - case SubCommand.Examples: - break; - case SubCommand.Concepts: - this.OutputHelp(monitor, SubCommand.Concepts); - break; - case SubCommand.Enable: - SCore.PerformanceCounterManager.EnableTracking = true; - monitor.Log("Performance counter tracking is now enabled", LogLevel.Info); - break; - case SubCommand.Disable: - SCore.PerformanceCounterManager.EnableTracking = false; - monitor.Log("Performance counter tracking is now disabled", LogLevel.Info); - break; - case SubCommand.Help: - if (args.TryGet(1, "command", out string commandString)) - this.OutputHelp(monitor, this.ParseCommandString(commandString)); - break; - default: - this.LogUsageError(monitor, $"Unknown command {subCommandString}"); - break; + this.LogUsageError(monitor, $"Unknown command {subcommandStr}"); + return; } } - else - this.HandleSummarySubCommand(monitor, args); + + // handle + switch (subcommand) + { + case SubCommand.Summary: + this.HandleSummarySubCommand(monitor, args); + break; + + case SubCommand.Detail: + this.HandleDetailSubCommand(monitor, args); + break; + + case SubCommand.Reset: + this.HandleResetSubCommand(monitor, args); + break; + + case SubCommand.Trigger: + this.HandleTriggerSubCommand(monitor, args); + break; + + case SubCommand.Enable: + SCore.PerformanceMonitor.EnableTracking = true; + monitor.Log("Performance counter tracking is now enabled", LogLevel.Info); + break; + + case SubCommand.Disable: + SCore.PerformanceMonitor.EnableTracking = false; + monitor.Log("Performance counter tracking is now disabled", LogLevel.Info); + break; + + case SubCommand.Help: + this.OutputHelp(monitor, args.TryGet(1, "command", out _) ? subcommand : null as SubCommand?); + break; + + default: + this.LogUsageError(monitor, $"Unknown command {subcommand}"); + break; + } } + + /********* + ** Private methods + *********/ /// Handles the summary sub command. /// Writes messages to the console and log file. /// The command arguments. private void HandleSummarySubCommand(IMonitor monitor, ArgumentParser args) { - IEnumerable data; - if (!args.TryGet(1, "mode", out string mode, false)) - { mode = "important"; - } + IEnumerable data = SCore.PerformanceMonitor.GetCollections(); switch (mode) { case null: case "important": - data = SCore.PerformanceCounterManager.PerformanceCounterCollections.Where(p => p.IsImportant); + data = data.Where(p => p.IsPerformanceCritical); break; + case "all": - data = SCore.PerformanceCounterManager.PerformanceCounterCollections; break; + default: - data = SCore.PerformanceCounterManager.PerformanceCounterCollections.Where(p => - p.Name.ToLowerInvariant().Contains(mode.ToLowerInvariant())); + data = data.Where(p => p.Name.ToLowerInvariant().Contains(mode.ToLowerInvariant())); break; } double? threshold = null; - if (args.TryGetDecimal(2, "threshold", out decimal t, false)) - { - threshold = (double?) t; - } - - StringBuilder sb = new StringBuilder(); + threshold = (double?)t; TimeSpan interval = TimeSpan.FromSeconds(60); - sb.AppendLine($"Summary over the last {interval.TotalSeconds} seconds:"); - sb.AppendLine(this.GetTableString( + StringBuilder report = new StringBuilder(); + report.AppendLine($"Summary over the last {interval.TotalSeconds} seconds:"); + report.AppendLine(this.GetTableString( data: data, - header: new[] {"Collection", "Avg Calls/s", "Avg Exec Time (Game)", "Avg Exec Time (Mods)", "Avg Exec Time (Game+Mods)", "Peak Exec Time"}, + header: new[] { "Collection", "Avg Calls/s", "Avg Exec Time (Game)", "Avg Exec Time (Mods)", "Avg Exec Time (Game+Mods)", "Peak Exec Time" }, getRow: item => new[] { item.Name, @@ -148,7 +139,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other true )); - monitor.Log(sb.ToString(), LogLevel.Info); + monitor.Log(report.ToString(), LogLevel.Info); } /// Handles the detail sub command. @@ -163,26 +154,16 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other if (args.TryGet(1, "collection", out string collectionName)) { - collections.AddRange(SCore.PerformanceCounterManager.PerformanceCounterCollections.Where( - collection => collection.Name.ToLowerInvariant().Contains(collectionName.ToLowerInvariant()))); + collections.AddRange(SCore.PerformanceMonitor.GetCollections().Where(collection => collection.Name.ToLowerInvariant().Contains(collectionName.ToLowerInvariant()))); - if (args.IsDecimal(2) && args.TryGetDecimal(2, "threshold", out decimal value, false)) - { - thresholdMilliseconds = (double?) value; - } - else - { - if (args.TryGet(2, "source", out string sourceName, false)) - { - sourceFilter = sourceName; - } - } + if (args.Count >= 2 && decimal.TryParse(args[2], out _) && args.TryGetDecimal(2, "threshold", out decimal value, false)) + thresholdMilliseconds = (double?)value; + else if (args.TryGet(2, "source", out string sourceName, false)) + sourceFilter = sourceName; } foreach (PerformanceCounterCollection c in collections) - { this.OutputPerformanceCollectionDetail(monitor, c, averageInterval, thresholdMilliseconds, sourceFilter); - } } /// Handles the trigger sub command. @@ -197,46 +178,44 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other case "list": this.OutputAlertTriggers(monitor); break; + case "collection": if (args.TryGet(2, "name", out string collectionName)) { if (args.TryGetDecimal(3, "threshold", out decimal threshold)) { - if (args.TryGet(4, "source", out string source, false)) - { - this.ConfigureAlertTrigger(monitor, collectionName, source, threshold); - } - else - { - this.ConfigureAlertTrigger(monitor, collectionName, null, threshold); - } + if (!args.TryGet(4, "source", out string source, required: false)) + source = null; + this.ConfigureAlertTrigger(monitor, collectionName, source, threshold); } } break; + case "pause": - SCore.PerformanceCounterManager.PauseAlerts = true; - monitor.Log($"Alerts are now paused.", LogLevel.Info); + SCore.PerformanceMonitor.PauseAlerts = true; + monitor.Log("Alerts are now paused.", LogLevel.Info); break; + case "resume": - SCore.PerformanceCounterManager.PauseAlerts = false; - monitor.Log($"Alerts are now resumed.", LogLevel.Info); + SCore.PerformanceMonitor.PauseAlerts = false; + monitor.Log("Alerts are now resumed.", LogLevel.Info); break; + case "dump": this.OutputAlertTriggers(monitor, true); break; + case "clear": this.ClearAlertTriggers(monitor); break; + default: this.LogUsageError(monitor, $"Unknown mode {mode}. See 'pc help trigger' for usage."); break; } - } else - { this.OutputAlertTriggers(monitor); - } } /// Sets up an an alert trigger. @@ -246,7 +225,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other /// The trigger threshold, or 0 to remove. private void ConfigureAlertTrigger(IMonitor monitor, string collectionName, string sourceName, decimal threshold) { - foreach (PerformanceCounterCollection collection in SCore.PerformanceCounterManager.PerformanceCounterCollections) + foreach (PerformanceCounterCollection collection in SCore.PerformanceMonitor.GetCollections()) { if (collection.Name.ToLowerInvariant().Equals(collectionName.ToLowerInvariant())) { @@ -255,8 +234,8 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other if (threshold != 0) { collection.EnableAlerts = true; - collection.AlertThresholdMilliseconds = (double) threshold; - monitor.Log($"Set up alert triggering for '{collectionName}' with '{this.FormatMilliseconds((double?) threshold)}'", LogLevel.Info); + collection.AlertThresholdMilliseconds = (double)threshold; + monitor.Log($"Set up alert triggering for '{collectionName}' with '{this.FormatMilliseconds((double?)threshold)}'", LogLevel.Info); } else { @@ -275,14 +254,11 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other if (threshold != 0) { performanceCounter.Value.EnableAlerts = true; - performanceCounter.Value.AlertThresholdMilliseconds = (double) threshold; - monitor.Log($"Set up alert triggering for '{sourceName}' in collection '{collectionName}' with '{this.FormatMilliseconds((double?) threshold)}", LogLevel.Info); + performanceCounter.Value.AlertThresholdMilliseconds = (double)threshold; + monitor.Log($"Set up alert triggering for '{sourceName}' in collection '{collectionName}' with '{this.FormatMilliseconds((double?)threshold)}", LogLevel.Info); } else - { performanceCounter.Value.EnableAlerts = false; - } - return; } } @@ -302,7 +278,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other private void ClearAlertTriggers(IMonitor monitor) { int clearedTriggers = 0; - foreach (PerformanceCounterCollection collection in SCore.PerformanceCounterManager.PerformanceCounterCollections) + foreach (PerformanceCounterCollection collection in SCore.PerformanceMonitor.GetCollections()) { if (collection.EnableAlerts) { @@ -329,92 +305,75 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other /// True to dump the triggers as commands. private void OutputAlertTriggers(IMonitor monitor, bool asDump = false) { - StringBuilder sb = new StringBuilder(); - sb.AppendLine("Configured triggers:"); - sb.AppendLine(); - var collectionTriggers = new List<(string collectionName, double threshold)>(); - var sourceTriggers = new List<(string collectionName, string sourceName, double threshold)>(); + StringBuilder report = new StringBuilder(); + report.AppendLine("Configured triggers:"); + report.AppendLine(); + var collectionTriggers = new List(); + var sourceTriggers = new List(); - foreach (PerformanceCounterCollection collection in SCore.PerformanceCounterManager.PerformanceCounterCollections) + foreach (PerformanceCounterCollection collection in SCore.PerformanceMonitor.GetCollections()) { if (collection.EnableAlerts) - { - collectionTriggers.Add((collection.Name, collection.AlertThresholdMilliseconds)); - } + collectionTriggers.Add(new CollectionTrigger(collection.Name, collection.AlertThresholdMilliseconds)); - sourceTriggers.AddRange(from performanceCounter in - collection.PerformanceCounters where performanceCounter.Value.EnableAlerts - select (collection.Name, performanceCounter.Value.Source, performanceCounter.Value.AlertThresholdMilliseconds)); + sourceTriggers.AddRange( + from counter in collection.PerformanceCounters + where counter.Value.EnableAlerts + select new SourceTrigger(collection.Name, counter.Value.Source, counter.Value.AlertThresholdMilliseconds) + ); } if (collectionTriggers.Count > 0) { - sb.AppendLine("Collection Triggers:"); - sb.AppendLine(); + report.AppendLine("Collection Triggers:"); + report.AppendLine(); if (asDump) { foreach (var item in collectionTriggers) - { - sb.AppendLine($"pc trigger {item.collectionName} {item.threshold}"); - } + report.AppendLine($"pc trigger {item.CollectionName} {item.Threshold}"); } else { - sb.AppendLine(this.GetTableString( + report.AppendLine(this.GetTableString( data: collectionTriggers, - header: new[] {"Collection", "Threshold"}, - getRow: item => new[] - { - item.collectionName, - this.FormatMilliseconds(item.threshold) - }, + header: new[] { "Collection", "Threshold" }, + getRow: item => new[] { item.CollectionName, this.FormatMilliseconds(item.Threshold) }, true )); } - sb.AppendLine(); + report.AppendLine(); } else - { - sb.AppendLine("No collection triggers."); - } + report.AppendLine("No collection triggers."); if (sourceTriggers.Count > 0) { - sb.AppendLine("Source Triggers:"); - sb.AppendLine(); + report.AppendLine("Source Triggers:"); + report.AppendLine(); if (asDump) { - foreach (var item in sourceTriggers) - { - sb.AppendLine($"pc trigger {item.collectionName} {item.threshold} {item.sourceName}"); - } + foreach (SourceTrigger item in sourceTriggers) + report.AppendLine($"pc trigger {item.CollectionName} {item.Threshold} {item.SourceName}"); } else { - sb.AppendLine(this.GetTableString( + report.AppendLine(this.GetTableString( data: sourceTriggers, - header: new[] {"Collection", "Source", "Threshold"}, - getRow: item => new[] - { - item.collectionName, - item.sourceName, - this.FormatMilliseconds(item.threshold) - }, + header: new[] { "Collection", "Source", "Threshold" }, + getRow: item => new[] { item.CollectionName, item.SourceName, this.FormatMilliseconds(item.Threshold) }, true )); } - sb.AppendLine(); + report.AppendLine(); } else - { - sb.AppendLine("No source triggers."); - } + report.AppendLine("No source triggers."); - monitor.Log(sb.ToString(), LogLevel.Info); + monitor.Log(report.ToString(), LogLevel.Info); } /// Handles the reset sub command. @@ -422,25 +381,25 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other /// The command arguments. private void HandleResetSubCommand(IMonitor monitor, ArgumentParser args) { - if (args.TryGet(1, "type", out string type, false, new []{"category", "source"})) + if (args.TryGet(1, "type", out string type, false, new[] { "category", "source" })) { args.TryGet(2, "name", out string name); switch (type) { case "category": - SCore.PerformanceCounterManager.ResetCollection(name); + SCore.PerformanceMonitor.ResetCollection(name); monitor.Log($"All performance counters for category {name} are now cleared.", LogLevel.Info); break; case "source": - SCore.PerformanceCounterManager.ResetSource(name); + SCore.PerformanceMonitor.ResetSource(name); monitor.Log($"All performance counters for source {name} are now cleared.", LogLevel.Info); break; } } else { - SCore.PerformanceCounterManager.Reset(); + SCore.PerformanceMonitor.Reset(); monitor.Log("All performance counters are now cleared.", LogLevel.Info); } } @@ -455,7 +414,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other private void OutputPerformanceCollectionDetail(IMonitor monitor, PerformanceCounterCollection collection, TimeSpan averageInterval, double? thresholdMilliseconds, string sourceFilter = null) { - StringBuilder sb = new StringBuilder($"Performance Counter for {collection.Name}:\n\n"); + StringBuilder report = new StringBuilder($"Performance Counter for {collection.Name}:\n\n"); List> data = collection.PerformanceCounters.ToList(); @@ -466,15 +425,13 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other } if (thresholdMilliseconds != null) - { data = data.Where(p => p.Value.GetAverage(averageInterval) >= thresholdMilliseconds).ToList(); - } if (data.Any()) { - sb.AppendLine(this.GetTableString( + report.AppendLine(this.GetTableString( data: data, - header: new[] {"Mod", $"Avg Exec Time (last {(int) averageInterval.TotalSeconds}s)", "Last Exec Time", "Peak Exec Time", $"Peak Exec Time (last {(int) averageInterval.TotalSeconds}s)"}, + header: new[] { "Mod", $"Avg Exec Time (last {(int)averageInterval.TotalSeconds}s)", "Last Exec Time", "Peak Exec Time", $"Peak Exec Time (last {(int)averageInterval.TotalSeconds}s)" }, getRow: item => new[] { item.Key, @@ -488,28 +445,13 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other } else { - sb.Clear(); - sb.AppendLine($"Performance Counter for {collection.Name}: none."); - } - - monitor.Log(sb.ToString(), LogLevel.Info); - } - - /// Parses a command string and returns the associated command. - /// The command string - /// The parsed command. - private SubCommand ParseCommandString(string commandString) - { - foreach (var i in this.SubCommandNames.Where(i => - i.Value.Any(str => str.Equals(commandString, StringComparison.InvariantCultureIgnoreCase)))) - { - return i.Key; + report.Clear(); + report.AppendLine($"Performance Counter for {collection.Name}: none."); } - return SubCommand.None; + monitor.Log(report.ToString(), LogLevel.Info); } - /// Formats the given milliseconds value into a string format. Optionally /// allows a threshold to return "-" if the value is less than the threshold. /// The milliseconds to format. Returns "-" if null @@ -518,175 +460,212 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other private string FormatMilliseconds(double? milliseconds, double? thresholdMilliseconds = null) { if (milliseconds == null || (thresholdMilliseconds != null && milliseconds < thresholdMilliseconds)) - { return "-"; - } - return ((double) milliseconds).ToString("F2"); + return ((double)milliseconds).ToString("F2"); } /// Shows detailed help for a specific sub command. - /// The output monitor - /// The sub command - private void OutputHelp(IMonitor monitor, SubCommand subCommand) + /// The output monitor. + /// The subcommand. + private void OutputHelp(IMonitor monitor, SubCommand? subcommand) { - StringBuilder sb = new StringBuilder(); - sb.AppendLine(); + StringBuilder report = new StringBuilder(); + report.AppendLine(); - switch (subCommand) + switch (subcommand) { - case SubCommand.Concepts: - sb.AppendLine("A performance counter is a metric which measures execution time. Each performance"); - sb.AppendLine("counter consists of:"); - sb.AppendLine(); - sb.AppendLine(" - A source, which typically is a mod or the game itself."); - sb.AppendLine(" - A ring buffer which stores the data points (execution time and time when it was executed)"); - sb.AppendLine(); - sb.AppendLine("A set of performance counters is organized in a collection to group various areas."); - sb.AppendLine("Per default, collections for all game events [1] are created."); - sb.AppendLine(); - sb.AppendLine("Example:"); - sb.AppendLine(); - sb.AppendLine("The performance counter collection named 'Display.Rendered' contains one performance"); - sb.AppendLine("counters when the game executes the 'Display.Rendered' event, and one additional"); - sb.AppendLine("performance counter for each mod which handles the 'Display.Rendered' event."); - sb.AppendLine(); - sb.AppendLine("[1] https://stardewvalleywiki.com/Modding:Modder_Guide/APIs/Events"); - break; case SubCommand.Detail: - sb.AppendLine("Usage: pc detail "); - sb.AppendLine(" pc detail "); - sb.AppendLine(); - sb.AppendLine("Displays details for a specific collection."); - sb.AppendLine(); - sb.AppendLine("Arguments:"); - sb.AppendLine(" Required. The full or partial name of the collection to display."); - sb.AppendLine(" Optional. The full or partial name of the source."); - sb.AppendLine(" Optional. The threshold in milliseconds. Any average execution time below that"); - sb.AppendLine(" threshold is not reported."); - sb.AppendLine(); - sb.AppendLine("Examples:"); - sb.AppendLine("pc detail Display.Rendering Displays all performance counters for the 'Display.Rendering' collection"); - sb.AppendLine("pc detail Display.Rendering Pathoschild.ChestsAnywhere Displays the 'Display.Rendering' performance counter for 'Pathoschild.ChestsAnywhere'"); - sb.AppendLine("pc detail Display.Rendering 5 Displays the 'Display.Rendering' performance counters exceeding an average of 5ms"); + report.AppendLine("Usage: pc detail "); + report.AppendLine(" pc detail "); + report.AppendLine(); + report.AppendLine("Displays details for a specific collection."); + report.AppendLine(); + report.AppendLine("Arguments:"); + report.AppendLine(" Required. The full or partial name of the collection to display."); + report.AppendLine(" Optional. The full or partial name of the source."); + report.AppendLine(" Optional. The threshold in milliseconds. Any average execution time below that"); + report.AppendLine(" threshold is not reported."); + report.AppendLine(); + report.AppendLine("Examples:"); + report.AppendLine("pc detail Display.Rendering Displays all performance counters for the 'Display.Rendering' collection"); + report.AppendLine("pc detail Display.Rendering Pathoschild.ChestsAnywhere Displays the 'Display.Rendering' performance counter for 'Pathoschild.ChestsAnywhere'"); + report.AppendLine("pc detail Display.Rendering 5 Displays the 'Display.Rendering' performance counters exceeding an average of 5ms"); break; + case SubCommand.Summary: - sb.AppendLine("Usage: pc summary "); - sb.AppendLine(); - sb.AppendLine("Displays the performance counter summary."); - sb.AppendLine(); - sb.AppendLine("Arguments:"); - sb.AppendLine(" Optional. Defaults to 'important' if omitted. Specifies one of these modes:"); - sb.AppendLine(" - all Displays performance counters from all collections"); - sb.AppendLine(" - important Displays only important performance counter collections"); - sb.AppendLine(); - sb.AppendLine(" Optional. Only shows performance counter collections matching the given name"); - sb.AppendLine(" Optional. Hides the actual execution time if it is below this threshold"); - sb.AppendLine(); - sb.AppendLine("Examples:"); - sb.AppendLine("pc summary all Shows all events"); - sb.AppendLine("pc summary all 5 Shows all events"); - sb.AppendLine("pc summary Display.Rendering Shows only the 'Display.Rendering' collection"); + report.AppendLine("Usage: pc summary "); + report.AppendLine(); + report.AppendLine("Displays the performance counter summary."); + report.AppendLine(); + report.AppendLine("Arguments:"); + report.AppendLine(" Optional. Defaults to 'important' if omitted. Specifies one of these modes:"); + report.AppendLine(" - all Displays performance counters from all collections"); + report.AppendLine(" - important Displays only important performance counter collections"); + report.AppendLine(); + report.AppendLine(" Optional. Only shows performance counter collections matching the given name"); + report.AppendLine(" Optional. Hides the actual execution time if it is below this threshold"); + report.AppendLine(); + report.AppendLine("Examples:"); + report.AppendLine("pc summary all Shows all events"); + report.AppendLine("pc summary all 5 Shows all events"); + report.AppendLine("pc summary Display.Rendering Shows only the 'Display.Rendering' collection"); break; + case SubCommand.Trigger: - sb.AppendLine("Usage: pc trigger "); - sb.AppendLine("Usage: pc trigger collection "); - sb.AppendLine("Usage: pc trigger collection "); - sb.AppendLine(); - sb.AppendLine("Manages alert triggers."); - sb.AppendLine(); - sb.AppendLine("Arguments:"); - sb.AppendLine(" Optional. Specifies if a specific source or a specific collection should be triggered."); - sb.AppendLine(" - list Lists current triggers"); - sb.AppendLine(" - collection Sets up a trigger for a collection"); - sb.AppendLine(" - clear Clears all trigger entries"); - sb.AppendLine(" - pause Pauses triggering of alerts"); - sb.AppendLine(" - resume Resumes triggering of alerts"); - sb.AppendLine(" - dump Dumps all triggers as commands for copy and paste"); - sb.AppendLine(" Defaults to 'list' if not specified."); - sb.AppendLine(); - sb.AppendLine(" Required if the mode 'collection' is specified."); - sb.AppendLine(" Specifies the name of the collection to be triggered. Must be an exact match."); - sb.AppendLine(); - sb.AppendLine(" Optional. Specifies the name of a specific source. Must be an exact match."); - sb.AppendLine(); - sb.AppendLine(" Required if the mode 'collection' is specified."); - sb.AppendLine(" Specifies the threshold in milliseconds (fractions allowed)."); - sb.AppendLine(" Specify '0' to remove the threshold."); - sb.AppendLine(); - sb.AppendLine("Examples:"); - sb.AppendLine(); - sb.AppendLine("pc trigger collection Display.Rendering 10"); - sb.AppendLine(" Sets up an alert trigger which writes on the console if the execution time of all performance counters in"); - sb.AppendLine(" the 'Display.Rendering' collection exceed 10 milliseconds."); - sb.AppendLine(); - sb.AppendLine("pc trigger collection Display.Rendering 5 Pathoschild.ChestsAnywhere"); - sb.AppendLine(" Sets up an alert trigger to write on the console if the execution time of Pathoschild.ChestsAnywhere in"); - sb.AppendLine(" the 'Display.Rendering' collection exceed 5 milliseconds."); - sb.AppendLine(); - sb.AppendLine("pc trigger collection Display.Rendering 0"); - sb.AppendLine(" Removes the threshold previously defined from the collection. Note that source-specific thresholds are left intact."); - sb.AppendLine(); - sb.AppendLine("pc trigger clear"); - sb.AppendLine(" Clears all previously setup alert triggers."); + report.AppendLine("Usage: pc trigger "); + report.AppendLine("Usage: pc trigger collection "); + report.AppendLine("Usage: pc trigger collection "); + report.AppendLine(); + report.AppendLine("Manages alert triggers."); + report.AppendLine(); + report.AppendLine("Arguments:"); + report.AppendLine(" Optional. Specifies if a specific source or a specific collection should be triggered."); + report.AppendLine(" - list Lists current triggers"); + report.AppendLine(" - collection Sets up a trigger for a collection"); + report.AppendLine(" - clear Clears all trigger entries"); + report.AppendLine(" - pause Pauses triggering of alerts"); + report.AppendLine(" - resume Resumes triggering of alerts"); + report.AppendLine(" - dump Dumps all triggers as commands for copy and paste"); + report.AppendLine(" Defaults to 'list' if not specified."); + report.AppendLine(); + report.AppendLine(" Required if the mode 'collection' is specified."); + report.AppendLine(" Specifies the name of the collection to be triggered. Must be an exact match."); + report.AppendLine(); + report.AppendLine(" Optional. Specifies the name of a specific source. Must be an exact match."); + report.AppendLine(); + report.AppendLine(" Required if the mode 'collection' is specified."); + report.AppendLine(" Specifies the threshold in milliseconds (fractions allowed)."); + report.AppendLine(" Specify '0' to remove the threshold."); + report.AppendLine(); + report.AppendLine("Examples:"); + report.AppendLine(); + report.AppendLine("pc trigger collection Display.Rendering 10"); + report.AppendLine(" Sets up an alert trigger which writes on the console if the execution time of all performance counters in"); + report.AppendLine(" the 'Display.Rendering' collection exceed 10 milliseconds."); + report.AppendLine(); + report.AppendLine("pc trigger collection Display.Rendering 5 Pathoschild.ChestsAnywhere"); + report.AppendLine(" Sets up an alert trigger to write on the console if the execution time of Pathoschild.ChestsAnywhere in"); + report.AppendLine(" the 'Display.Rendering' collection exceed 5 milliseconds."); + report.AppendLine(); + report.AppendLine("pc trigger collection Display.Rendering 0"); + report.AppendLine(" Removes the threshold previously defined from the collection. Note that source-specific thresholds are left intact."); + report.AppendLine(); + report.AppendLine("pc trigger clear"); + report.AppendLine(" Clears all previously setup alert triggers."); break; + case SubCommand.Reset: - sb.AppendLine("Usage: pc reset "); - sb.AppendLine(); - sb.AppendLine("Resets performance counters."); - sb.AppendLine(); - sb.AppendLine("Arguments:"); - sb.AppendLine(" Optional. Specifies if a collection or source should be reset."); - sb.AppendLine(" If omitted, all performance counters are reset."); - sb.AppendLine(); - sb.AppendLine(" - source Clears performance counters for a specific source"); - sb.AppendLine(" - collection Clears performance counters for a specific collection"); - sb.AppendLine(); - sb.AppendLine(" Required if a is given. Specifies the name of either the collection"); - sb.AppendLine(" or the source. The name must be an exact match."); - sb.AppendLine(); - sb.AppendLine("Examples:"); - sb.AppendLine("pc reset Resets all performance counters"); - sb.AppendLine("pc reset source Pathoschild.ChestsAnywhere Resets all performance for the source named Pathoschild.ChestsAnywhere"); - sb.AppendLine("pc reset collection Display.Rendering Resets all performance for the collection named Display.Rendering"); + report.AppendLine("Usage: pc reset "); + report.AppendLine(); + report.AppendLine("Resets performance counters."); + report.AppendLine(); + report.AppendLine("Arguments:"); + report.AppendLine(" Optional. Specifies if a collection or source should be reset."); + report.AppendLine(" If omitted, all performance counters are reset."); + report.AppendLine(); + report.AppendLine(" - source Clears performance counters for a specific source"); + report.AppendLine(" - collection Clears performance counters for a specific collection"); + report.AppendLine(); + report.AppendLine(" Required if a is given. Specifies the name of either the collection"); + report.AppendLine(" or the source. The name must be an exact match."); + report.AppendLine(); + report.AppendLine("Examples:"); + report.AppendLine("pc reset Resets all performance counters"); + report.AppendLine("pc reset source Pathoschild.ChestsAnywhere Resets all performance for the source named Pathoschild.ChestsAnywhere"); + report.AppendLine("pc reset collection Display.Rendering Resets all performance for the collection named Display.Rendering"); break; } - sb.AppendLine(); - monitor.Log(sb.ToString(), LogLevel.Info); + report.AppendLine(); + monitor.Log(report.ToString(), LogLevel.Info); } /// Get the command description. private static string GetDescription() { - StringBuilder sb = new StringBuilder(); - - sb.AppendLine("Displays and configures performance counters."); - sb.AppendLine(); - sb.AppendLine("A performance counter records the invocation time of in-game events being"); - sb.AppendLine("processed by mods or the game itself. See 'concepts' for a detailed explanation."); - sb.AppendLine(); - sb.AppendLine("Usage: pc "); - sb.AppendLine(); - sb.AppendLine("Commands:"); - sb.AppendLine(); - sb.AppendLine(" summary|sum|s Displays a summary of important or all collections"); - sb.AppendLine(" detail|d Shows performance counter information for a given collection"); - sb.AppendLine(" reset|r Resets the performance counters"); - sb.AppendLine(" trigger Configures alert triggers"); - sb.AppendLine(" enable Enables performance counter recording"); - sb.AppendLine(" disable Disables performance counter recording"); - sb.AppendLine(" examples Displays various examples"); - sb.AppendLine(" concepts Displays an explanation of the performance counter concepts"); - sb.AppendLine(" help Displays verbose help for the available commands"); - sb.AppendLine(); - sb.AppendLine("To get help for a specific command, use 'pc help ', for example:"); - sb.AppendLine("pc help summary"); - sb.AppendLine(); - sb.AppendLine("Defaults to summary if no command is given."); - sb.AppendLine(); - - return sb.ToString(); + StringBuilder report = new StringBuilder(); + + report.AppendLine("Displays or configures performance monitoring for diagnose issues."); + report.AppendLine(); + report.AppendLine("A 'performance counter' is a metric which measures execution time across a range of time for a source (e.g. a mod)."); + report.AppendLine("A set of performance counters is organized in a collection to group various areas."); + report.AppendLine("For example, the performance counter collection named 'Display.Rendered' contains one performance"); + report.AppendLine("counter when the game executes the 'Display.Rendered' event, and another counter for each mod which handles it."); + report.AppendLine(); + report.AppendLine("Usage: pc "); + report.AppendLine(); + report.AppendLine("Commands:"); + report.AppendLine(); + report.AppendLine(" summary Show a summary of collections."); + report.AppendLine(" detail Show a summary for a given collection."); + report.AppendLine(" reset Reset all performance counters."); + report.AppendLine(" trigger Configure alert triggers."); + report.AppendLine(" enable Enable performance counter recording."); + report.AppendLine(" disable Disable performance counter recording."); + report.AppendLine(" help Show verbose help for the available commands."); + report.AppendLine(); + report.AppendLine("To get help for a specific command, use 'pc help ', for example:"); + report.AppendLine("pc help summary"); + report.AppendLine(); + report.AppendLine("Defaults to summary if no command is given."); + report.AppendLine(); + + return report.ToString(); + } + + + /********* + ** Private models + *********/ + /// An alert trigger for a collection. + private class CollectionTrigger + { + /********* + ** Accessors + *********/ + /// The collection name. + public string CollectionName { get; } + + /// The trigger threshold. + public double Threshold { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The collection name. + /// The trigger threshold. + public CollectionTrigger(string collectionName, double threshold) + { + this.CollectionName = collectionName; + this.Threshold = threshold; + } + } + + /// An alert triggered for a source. + private class SourceTrigger : CollectionTrigger + { + /********* + ** Accessors + *********/ + /// The source name. + public string SourceName { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The collection name. + /// The source name. + /// The trigger threshold. + public SourceTrigger(string collectionName, string sourceName, double threshold) + : base(collectionName, threshold) + { + this.SourceName = sourceName; + } } } } diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/TrainerCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/TrainerCommand.cs index 8f0d89ba..2b562a08 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/TrainerCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/TrainerCommand.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; @@ -66,7 +66,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands /// The data to display. /// The table header. /// Returns a set of fields for a data value. - /// True to right-align the data, false for left-align. Default false. + /// Whether to right-align the data. protected string GetTableString(IEnumerable data, string[] header, Func getRow, bool rightAlign = false) { // get table data @@ -93,19 +93,11 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands }; lines.AddRange(rows); - if (rightAlign) - { - return string.Join( - Environment.NewLine, - lines.Select(line => string.Join(" | ", line.Select((field, i) => field.PadLeft(widths[i], ' ')).ToArray()) - ) - ); - } - return string.Join( Environment.NewLine, - lines.Select(line => string.Join(" | ", line.Select((field, i) => field.PadRight(widths[i], ' ')).ToArray()) - ) + lines.Select(line => string.Join(" | ", + line.Select((field, i) => rightAlign ? field.PadRight(widths[i], ' ') : field.PadLeft(widths[i], ' ')) + )) ); } } diff --git a/src/SMAPI.Mods.ConsoleCommands/SMAPI.Mods.ConsoleCommands.csproj b/src/SMAPI.Mods.ConsoleCommands/SMAPI.Mods.ConsoleCommands.csproj index f073ac21..ce35bf73 100644 --- a/src/SMAPI.Mods.ConsoleCommands/SMAPI.Mods.ConsoleCommands.csproj +++ b/src/SMAPI.Mods.ConsoleCommands/SMAPI.Mods.ConsoleCommands.csproj @@ -67,10 +67,6 @@ - - - - diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index 76cb6f89..67c7b576 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -55,7 +55,7 @@ namespace StardewModdingAPI /// The URL of the SMAPI home page. internal const string HomePageUrl = "https://smapi.io"; - /// The URL of the SMAPI home page. + /// The default performance counter name for unknown event handlers. internal const string GamePerformanceCounterName = ""; /// The absolute path to the folder containing SMAPI's internal files. @@ -103,6 +103,7 @@ namespace StardewModdingAPI /// The language code for non-translated mod assets. internal static LocalizedContentManager.LanguageCode DefaultLanguage { get; } = LocalizedContentManager.LanguageCode.en; + /********* ** Internal methods *********/ diff --git a/src/SMAPI/Framework/Events/EventManager.cs b/src/SMAPI/Framework/Events/EventManager.cs index 19a4dff8..50dcc9ef 100644 --- a/src/SMAPI/Framework/Events/EventManager.cs +++ b/src/SMAPI/Framework/Events/EventManager.cs @@ -1,4 +1,6 @@ +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Reflection; using StardewModdingAPI.Events; using StardewModdingAPI.Framework.PerformanceCounter; @@ -174,29 +176,32 @@ namespace StardewModdingAPI.Framework.Events /// Construct an instance. /// Writes messages to the log. /// The mod registry with which to identify mods. - /// The performance counter manager. - public EventManager(IMonitor monitor, ModRegistry modRegistry, PerformanceCounterManager performanceCounterManager) + /// Tracks performance metrics. + public EventManager(IMonitor monitor, ModRegistry modRegistry, PerformanceMonitor performanceMonitor) { // create shortcut initializers - ManagedEvent ManageEventOf(string typeName, string eventName) => new ManagedEvent($"{typeName}.{eventName}", monitor, modRegistry, performanceCounterManager); + ManagedEvent ManageEventOf(string typeName, string eventName, bool isPerformanceCritical = false) + { + return new ManagedEvent($"{typeName}.{eventName}", monitor, modRegistry, performanceMonitor, isPerformanceCritical); + } // init events (new) this.MenuChanged = ManageEventOf(nameof(IModEvents.Display), nameof(IDisplayEvents.MenuChanged)); - this.Rendering = ManageEventOf(nameof(IModEvents.Display), nameof(IDisplayEvents.Rendering)); - this.Rendered = ManageEventOf(nameof(IModEvents.Display), nameof(IDisplayEvents.Rendered)); - this.RenderingWorld = ManageEventOf(nameof(IModEvents.Display), nameof(IDisplayEvents.RenderingWorld)); - this.RenderedWorld = ManageEventOf(nameof(IModEvents.Display), nameof(IDisplayEvents.RenderedWorld)); - this.RenderingActiveMenu = ManageEventOf(nameof(IModEvents.Display), nameof(IDisplayEvents.RenderingActiveMenu)); - this.RenderedActiveMenu = ManageEventOf(nameof(IModEvents.Display), nameof(IDisplayEvents.RenderedActiveMenu)); - this.RenderingHud = ManageEventOf(nameof(IModEvents.Display), nameof(IDisplayEvents.RenderingHud)); - this.RenderedHud = ManageEventOf(nameof(IModEvents.Display), nameof(IDisplayEvents.RenderedHud)); + this.Rendering = ManageEventOf(nameof(IModEvents.Display), nameof(IDisplayEvents.Rendering), isPerformanceCritical: true); + this.Rendered = ManageEventOf(nameof(IModEvents.Display), nameof(IDisplayEvents.Rendered), isPerformanceCritical: true); + this.RenderingWorld = ManageEventOf(nameof(IModEvents.Display), nameof(IDisplayEvents.RenderingWorld), isPerformanceCritical: true); + this.RenderedWorld = ManageEventOf(nameof(IModEvents.Display), nameof(IDisplayEvents.RenderedWorld), isPerformanceCritical: true); + this.RenderingActiveMenu = ManageEventOf(nameof(IModEvents.Display), nameof(IDisplayEvents.RenderingActiveMenu), isPerformanceCritical: true); + this.RenderedActiveMenu = ManageEventOf(nameof(IModEvents.Display), nameof(IDisplayEvents.RenderedActiveMenu), isPerformanceCritical: true); + this.RenderingHud = ManageEventOf(nameof(IModEvents.Display), nameof(IDisplayEvents.RenderingHud), isPerformanceCritical: true); + this.RenderedHud = ManageEventOf(nameof(IModEvents.Display), nameof(IDisplayEvents.RenderedHud), isPerformanceCritical: true); this.WindowResized = ManageEventOf(nameof(IModEvents.Display), nameof(IDisplayEvents.WindowResized)); this.GameLaunched = ManageEventOf(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.GameLaunched)); - this.UpdateTicking = ManageEventOf(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.UpdateTicking)); - this.UpdateTicked = ManageEventOf(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.UpdateTicked)); - this.OneSecondUpdateTicking = ManageEventOf(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.OneSecondUpdateTicking)); - this.OneSecondUpdateTicked = ManageEventOf(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.OneSecondUpdateTicked)); + this.UpdateTicking = ManageEventOf(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.UpdateTicking), isPerformanceCritical: true); + this.UpdateTicked = ManageEventOf(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.UpdateTicked), isPerformanceCritical: true); + this.OneSecondUpdateTicking = ManageEventOf(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.OneSecondUpdateTicking), isPerformanceCritical: true); + this.OneSecondUpdateTicked = ManageEventOf(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.OneSecondUpdateTicked), isPerformanceCritical: true); this.SaveCreating = ManageEventOf(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.SaveCreating)); this.SaveCreated = ManageEventOf(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.SaveCreated)); this.Saving = ManageEventOf(nameof(IModEvents.GameLoop), nameof(IGameLoopEvents.Saving)); @@ -209,7 +214,7 @@ namespace StardewModdingAPI.Framework.Events this.ButtonPressed = ManageEventOf(nameof(IModEvents.Input), nameof(IInputEvents.ButtonPressed)); this.ButtonReleased = ManageEventOf(nameof(IModEvents.Input), nameof(IInputEvents.ButtonReleased)); - this.CursorMoved = ManageEventOf(nameof(IModEvents.Input), nameof(IInputEvents.CursorMoved)); + this.CursorMoved = ManageEventOf(nameof(IModEvents.Input), nameof(IInputEvents.CursorMoved), isPerformanceCritical: true); this.MouseWheelScrolled = ManageEventOf(nameof(IModEvents.Input), nameof(IInputEvents.MouseWheelScrolled)); this.PeerContextReceived = ManageEventOf(nameof(IModEvents.Multiplayer), nameof(IMultiplayerEvents.PeerContextReceived)); @@ -230,8 +235,15 @@ namespace StardewModdingAPI.Framework.Events this.TerrainFeatureListChanged = ManageEventOf(nameof(IModEvents.World), nameof(IWorldEvents.TerrainFeatureListChanged)); this.LoadStageChanged = ManageEventOf(nameof(IModEvents.Specialized), nameof(ISpecializedEvents.LoadStageChanged)); - this.UnvalidatedUpdateTicking = ManageEventOf(nameof(IModEvents.Specialized), nameof(ISpecializedEvents.UnvalidatedUpdateTicking)); - this.UnvalidatedUpdateTicked = ManageEventOf(nameof(IModEvents.Specialized), nameof(ISpecializedEvents.UnvalidatedUpdateTicked)); + this.UnvalidatedUpdateTicking = ManageEventOf(nameof(IModEvents.Specialized), nameof(ISpecializedEvents.UnvalidatedUpdateTicking), isPerformanceCritical: true); + this.UnvalidatedUpdateTicked = ManageEventOf(nameof(IModEvents.Specialized), nameof(ISpecializedEvents.UnvalidatedUpdateTicked), isPerformanceCritical: true); + } + + /// Get all managed events. + public IEnumerable GetAllEvents() + { + foreach (FieldInfo field in this.GetType().GetFields()) + yield return (IManagedEvent)field.GetValue(this); } } } diff --git a/src/SMAPI/Framework/Events/IManagedEvent.cs b/src/SMAPI/Framework/Events/IManagedEvent.cs index 04476866..e4e3ca08 100644 --- a/src/SMAPI/Framework/Events/IManagedEvent.cs +++ b/src/SMAPI/Framework/Events/IManagedEvent.cs @@ -1,7 +1,15 @@ namespace StardewModdingAPI.Framework.Events { + /// Metadata for an event raised by SMAPI. internal interface IManagedEvent { - string GetName(); + /********* + ** Accessors + *********/ + /// A human-readable name for the event. + string EventName { get; } + + /// Whether the event is typically called at least once per second. + bool IsPerformanceCritical { get; } } } diff --git a/src/SMAPI/Framework/Events/ManagedEvent.cs b/src/SMAPI/Framework/Events/ManagedEvent.cs index dfdd7449..60e5c599 100644 --- a/src/SMAPI/Framework/Events/ManagedEvent.cs +++ b/src/SMAPI/Framework/Events/ManagedEvent.cs @@ -1,13 +1,13 @@ using System; using System.Collections.Generic; using System.Linq; -using PerformanceCounterManager = StardewModdingAPI.Framework.PerformanceCounter.PerformanceCounterManager; +using StardewModdingAPI.Framework.PerformanceCounter; namespace StardewModdingAPI.Framework.Events { /// An event wrapper which intercepts and logs errors in handler code. /// The event arguments type. - internal class ManagedEvent: IManagedEvent + internal class ManagedEvent : IManagedEvent { /********* ** Fields @@ -15,9 +15,6 @@ namespace StardewModdingAPI.Framework.Events /// The underlying event. private event EventHandler Event; - /// A human-readable name for the event. - private readonly string EventName; - /// Writes messages to the log. private readonly IMonitor Monitor; @@ -30,8 +27,19 @@ namespace StardewModdingAPI.Framework.Events /// The cached invocation list. private EventHandler[] CachedInvocationList; - /// The performance counter manager. - private readonly PerformanceCounterManager PerformanceCounterManager; + /// Tracks performance metrics. + private readonly PerformanceMonitor PerformanceMonitor; + + + /********* + ** Accessors + *********/ + /// A human-readable name for the event. + public string EventName { get; } + + /// Whether the event is typically called at least once per second. + public bool IsPerformanceCritical { get; } + /********* ** Public methods @@ -40,19 +48,15 @@ namespace StardewModdingAPI.Framework.Events /// A human-readable name for the event. /// Writes messages to the log. /// The mod registry with which to identify mods. - /// The performance counter manager - public ManagedEvent(string eventName, IMonitor monitor, ModRegistry modRegistry, PerformanceCounterManager performanceCounterManager) + /// Tracks performance metrics. + /// Whether the event is typically called at least once per second. + public ManagedEvent(string eventName, IMonitor monitor, ModRegistry modRegistry, PerformanceMonitor performanceMonitor, bool isPerformanceCritical = false) { this.EventName = eventName; this.Monitor = monitor; this.ModRegistry = modRegistry; - this.PerformanceCounterManager = performanceCounterManager; - } - - /// Gets the event name. - public string GetName() - { - return this.EventName; + this.PerformanceMonitor = performanceMonitor; + this.IsPerformanceCritical = isPerformanceCritical; } /// Get whether anything is listening to the event. @@ -93,22 +97,20 @@ namespace StardewModdingAPI.Framework.Events return; - this.PerformanceCounterManager.BeginTrackInvocation(this.EventName); - - foreach (EventHandler handler in this.CachedInvocationList) + this.PerformanceMonitor.Track(this.EventName, () => { - try - { - this.PerformanceCounterManager.Track(this.EventName, this.GetModNameForPerformanceCounters(handler), - () => handler.Invoke(null, args)); - } - catch (Exception ex) + foreach (EventHandler handler in this.CachedInvocationList) { - this.LogError(handler, ex); + try + { + this.PerformanceMonitor.Track(this.EventName, this.GetModNameForPerformanceCounters(handler), () => handler.Invoke(null, args)); + } + catch (Exception ex) + { + this.LogError(handler, ex); + } } - } - - this.PerformanceCounterManager.EndTrackInvocation(this.EventName); + }); } /// Raise the event and notify all handlers. @@ -139,18 +141,17 @@ namespace StardewModdingAPI.Framework.Events /********* ** Private methods *********/ - + /// Get the mod name for a given event handler to display in performance monitoring reports. + /// The event handler. private string GetModNameForPerformanceCounters(EventHandler handler) { IModMetadata mod = this.GetSourceMod(handler); - if (mod == null) - { return Constants.GamePerformanceCounterName; - } - - return mod.HasManifest() ? mod.Manifest.UniqueID : mod.DisplayName; + return mod.HasManifest() + ? mod.Manifest.UniqueID + : mod.DisplayName; } /// Track an event handler. diff --git a/src/SMAPI/Framework/PerformanceCounter/AlertContext.cs b/src/SMAPI/Framework/PerformanceCounter/AlertContext.cs index 63f0a5ed..76c472e1 100644 --- a/src/SMAPI/Framework/PerformanceCounter/AlertContext.cs +++ b/src/SMAPI/Framework/PerformanceCounter/AlertContext.cs @@ -3,13 +3,20 @@ namespace StardewModdingAPI.Framework.PerformanceCounter /// The context for an alert. internal struct AlertContext { + /********* + ** Accessors + *********/ /// The source which triggered the alert. - public readonly string Source; + public string Source { get; } /// The elapsed milliseconds. - public readonly double Elapsed; + public double Elapsed { get; } - /// Creates a new alert context. + + /********* + ** Public methods + *********/ + /// Construct an instance. /// The source which triggered the alert. /// The elapsed milliseconds. public AlertContext(string source, double elapsed) @@ -18,6 +25,7 @@ namespace StardewModdingAPI.Framework.PerformanceCounter this.Elapsed = elapsed; } + /// Get a human-readable text form of this instance. public override string ToString() { return $"{this.Source}: {this.Elapsed:F2}ms"; diff --git a/src/SMAPI/Framework/PerformanceCounter/AlertEntry.cs b/src/SMAPI/Framework/PerformanceCounter/AlertEntry.cs index b87d8642..494f34a9 100644 --- a/src/SMAPI/Framework/PerformanceCounter/AlertEntry.cs +++ b/src/SMAPI/Framework/PerformanceCounter/AlertEntry.cs @@ -1,28 +1,33 @@ -using System.Collections.Generic; - namespace StardewModdingAPI.Framework.PerformanceCounter { /// A single alert entry. internal struct AlertEntry { + /********* + ** Accessors + *********/ /// The collection in which the alert occurred. - public readonly PerformanceCounterCollection Collection; + public PerformanceCounterCollection Collection { get; } /// The actual execution time in milliseconds. - public readonly double ExecutionTimeMilliseconds; + public double ExecutionTimeMilliseconds { get; } + + /// The configured alert threshold in milliseconds. + public double ThresholdMilliseconds { get; } - /// The configured alert threshold. - public readonly double ThresholdMilliseconds; + /// The sources involved in exceeding the threshold. + public AlertContext[] Context { get; } - /// The context list, which records all sources involved in exceeding the threshold. - public readonly List Context; - /// Creates a new alert entry. - /// The source collection in which the alert occurred. + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The collection in which the alert occurred. /// The actual execution time in milliseconds. - /// The configured threshold in milliseconds. - /// A list of AlertContext to record which sources were involved - public AlertEntry(PerformanceCounterCollection collection, double executionTimeMilliseconds, double thresholdMilliseconds, List context) + /// The configured alert threshold in milliseconds. + /// The sources involved in exceeding the threshold. + public AlertEntry(PerformanceCounterCollection collection, double executionTimeMilliseconds, double thresholdMilliseconds, AlertContext[] context) { this.Collection = collection; this.ExecutionTimeMilliseconds = executionTimeMilliseconds; diff --git a/src/SMAPI/Framework/PerformanceCounter/EventPerformanceCounterCollection.cs b/src/SMAPI/Framework/PerformanceCounter/EventPerformanceCounterCollection.cs deleted file mode 100644 index 4690c512..00000000 --- a/src/SMAPI/Framework/PerformanceCounter/EventPerformanceCounterCollection.cs +++ /dev/null @@ -1,16 +0,0 @@ -using StardewModdingAPI.Framework.Events; - -namespace StardewModdingAPI.Framework.PerformanceCounter -{ - /// Represents a performance counter collection specific to game events. - internal class EventPerformanceCounterCollection: PerformanceCounterCollection - { - /// Creates a new event performance counter collection. - /// The performance counter manager. - /// The ManagedEvent. - /// If the event is flagged as important. - public EventPerformanceCounterCollection(PerformanceCounterManager manager, IManagedEvent @event, bool isImportant) : base(manager, @event.GetName(), isImportant) - { - } - } -} diff --git a/src/SMAPI/Framework/PerformanceCounter/PeakEntry.cs b/src/SMAPI/Framework/PerformanceCounter/PeakEntry.cs index 95dc11f4..abb29541 100644 --- a/src/SMAPI/Framework/PerformanceCounter/PeakEntry.cs +++ b/src/SMAPI/Framework/PerformanceCounter/PeakEntry.cs @@ -1,20 +1,31 @@ using System; -using System.Collections.Generic; namespace StardewModdingAPI.Framework.PerformanceCounter { + /// A peak invocation time. internal struct PeakEntry { + /********* + ** Accessors + *********/ /// The actual execution time in milliseconds. - public readonly double ExecutionTimeMilliseconds; + public double ExecutionTimeMilliseconds { get; } - /// The DateTime when the entry occured. - public DateTime EventTime; + /// When the entry occurred. + public DateTime EventTime { get; } - /// The context list, which records all sources involved in exceeding the threshold. - public readonly List Context; + /// The sources involved in exceeding the threshold. + public AlertContext[] Context { get; } - public PeakEntry(double executionTimeMilliseconds, DateTime eventTime, List context) + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The actual execution time in milliseconds. + /// When the entry occurred. + /// The sources involved in exceeding the threshold. + public PeakEntry(double executionTimeMilliseconds, DateTime eventTime, AlertContext[] context) { this.ExecutionTimeMilliseconds = executionTimeMilliseconds; this.EventTime = eventTime; diff --git a/src/SMAPI/Framework/PerformanceCounter/PerformanceCounter.cs b/src/SMAPI/Framework/PerformanceCounter/PerformanceCounter.cs index e9dfcb14..0fb6482d 100644 --- a/src/SMAPI/Framework/PerformanceCounter/PerformanceCounter.cs +++ b/src/SMAPI/Framework/PerformanceCounter/PerformanceCounter.cs @@ -1,23 +1,32 @@ using System; +using System.Collections.Generic; using System.Linq; -using Cyotek.Collections.Generic; +using Harmony; namespace StardewModdingAPI.Framework.PerformanceCounter { + /// Tracks metadata about a particular code event. internal class PerformanceCounter { + /********* + ** Fields + *********/ /// The size of the ring buffer. - private const int MAX_ENTRIES = 16384; + private readonly int MaxEntries = 16384; /// The collection to which this performance counter belongs. private readonly PerformanceCounterCollection ParentCollection; - /// The circular buffer which stores all performance counter entries - private readonly CircularBuffer _counter; + /// The performance counter entries. + private readonly Stack Entries; - /// The peak execution time + /// The entry with the highest execution time. private PerformanceCounterEntry? PeakPerformanceCounterEntry; + + /********* + ** Accessors + *********/ /// The name of the source. public string Source { get; } @@ -27,118 +36,90 @@ namespace StardewModdingAPI.Framework.PerformanceCounter /// If alerting is enabled or not public bool EnableAlerts { get; set; } + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The collection to which this performance counter belongs. + /// The name of the source. public PerformanceCounter(PerformanceCounterCollection parentCollection, string source) { this.ParentCollection = parentCollection; this.Source = source; - this._counter = new CircularBuffer(PerformanceCounter.MAX_ENTRIES); + this.Entries = new Stack(this.MaxEntries); } - /// Adds a new performance counter entry to the list. Updates the peak entry and adds an alert if - /// monitoring is enabled and the execution time exceeds the threshold. + /// Add a performance counter entry to the list, update monitoring, and raise alerts if needed. /// The entry to add. public void Add(PerformanceCounterEntry entry) { - this._counter.Put(entry); - - if (this.EnableAlerts && entry.ElapsedMilliseconds > this.AlertThresholdMilliseconds) - this.ParentCollection.AddAlert(entry.ElapsedMilliseconds, this.AlertThresholdMilliseconds, - new AlertContext(this.Source, entry.ElapsedMilliseconds)); + // add entry + if (this.Entries.Count > this.MaxEntries) + this.Entries.Pop(); + this.Entries.Add(entry); - if (this.PeakPerformanceCounterEntry == null) + // update metrics + if (this.PeakPerformanceCounterEntry == null || entry.ElapsedMilliseconds > this.PeakPerformanceCounterEntry.Value.ElapsedMilliseconds) this.PeakPerformanceCounterEntry = entry; - else - { - if (entry.ElapsedMilliseconds > this.PeakPerformanceCounterEntry.Value.ElapsedMilliseconds) - this.PeakPerformanceCounterEntry = entry; - } + + // raise alert + if (this.EnableAlerts && entry.ElapsedMilliseconds > this.AlertThresholdMilliseconds) + this.ParentCollection.AddAlert(entry.ElapsedMilliseconds, this.AlertThresholdMilliseconds, new AlertContext(this.Source, entry.ElapsedMilliseconds)); } - /// Clears all performance counter entries and resets the peak entry. + /// Clear all performance counter entries and monitoring. public void Reset() { - this._counter.Clear(); + this.Entries.Clear(); this.PeakPerformanceCounterEntry = null; } - /// Returns the peak entry. - /// The peak entry. + /// Get the peak entry. public PerformanceCounterEntry? GetPeak() { return this.PeakPerformanceCounterEntry; } - /// Returns the peak entry. - /// The peak entry. - public PerformanceCounterEntry? GetPeak(TimeSpan range, DateTime? relativeTo = null) + /// Get the entry with the highest execution time. + /// The time range to search. + /// The end time for the , or null for the current time. + public PerformanceCounterEntry? GetPeak(TimeSpan range, DateTime? endTime = null) { - if (this._counter.IsEmpty) - return null; - - if (relativeTo == null) - relativeTo = DateTime.UtcNow; - - DateTime start = relativeTo.Value.Subtract(range); - - var entries = this._counter.Where(x => (x.EventTime >= start) && (x.EventTime <= relativeTo)).ToList(); - - if (!entries.Any()) - return null; + endTime ??= DateTime.UtcNow; + DateTime startTime = endTime.Value.Subtract(range); - return entries.OrderByDescending(x => x.ElapsedMilliseconds).First(); + return this.Entries + .Where(entry => entry.EventTime >= startTime && entry.EventTime <= endTime) + .OrderByDescending(x => x.ElapsedMilliseconds) + .FirstOrDefault(); } - /// Resets the peak entry. - public void ResetPeak() - { - this.PeakPerformanceCounterEntry = null; - } - - /// Returns the last entry added to the list. - /// The last entry + /// Get the last entry added to the list. public PerformanceCounterEntry? GetLastEntry() { - if (this._counter.IsEmpty) + if (this.Entries.Count == 0) return null; - return this._counter.PeekLast(); + return this.Entries.Peek(); } - /// Returns the average execution time of all entries. - /// The average execution time in milliseconds. - public double GetAverage() + /// Get the average over a given time span. + /// The time range to search. + /// The end time for the , or null for the current time. + public double GetAverage(TimeSpan range, DateTime? endTime = null) { - if (this._counter.IsEmpty) - return 0; - - return this._counter.Average(p => p.ElapsedMilliseconds); - } - - /// Returns the average over a given time span. - /// The time range to retrieve. - /// The DateTime from which to start the average. Defaults to DateTime.UtcNow if null - /// The average execution time in milliseconds. - /// - /// The relativeTo parameter specifies from which point in time the range is subtracted. Example: - /// If DateTime is set to 60 seconds ago, and the range is set to 60 seconds, the method would return - /// the average between all entries between 120s ago and 60s ago. - /// - public double GetAverage(TimeSpan range, DateTime? relativeTo = null) - { - if (this._counter.IsEmpty) - return 0; - - if (relativeTo == null) - relativeTo = DateTime.UtcNow; - - DateTime start = relativeTo.Value.Subtract(range); - - var entries = this._counter.Where(x => (x.EventTime >= start) && (x.EventTime <= relativeTo)).ToList(); + endTime ??= DateTime.UtcNow; + DateTime startTime = endTime.Value.Subtract(range); - if (!entries.Any()) - return 0; + double[] entries = this.Entries + .Where(entry => entry.EventTime >= startTime && entry.EventTime <= endTime) + .Select(p => p.ElapsedMilliseconds) + .ToArray(); - return entries.Average(x => x.ElapsedMilliseconds); + return entries.Length > 0 + ? entries.Average() + : 0; } } } diff --git a/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterCollection.cs b/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterCollection.cs index f469eceb..bd13a36e 100644 --- a/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterCollection.cs +++ b/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterCollection.cs @@ -2,153 +2,129 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; -using Cyotek.Collections.Generic; namespace StardewModdingAPI.Framework.PerformanceCounter { internal class PerformanceCounterCollection { - /// The size of the ring buffer. - private const int MAX_ENTRIES = 16384; + /********* + ** Fields + *********/ + /// The number of peak invocations to keep. + private readonly int MaxEntries = 16384; - /// The list of triggered performance counters. + /// The sources involved in exceeding alert thresholds. private readonly List TriggeredPerformanceCounters = new List(); /// The stopwatch used to track the invocation time. private readonly Stopwatch InvocationStopwatch = new Stopwatch(); /// The performance counter manager. - private readonly PerformanceCounterManager PerformanceCounterManager; + private readonly PerformanceMonitor PerformanceMonitor; - /// Holds the time to calculate the average calls per second. + /// The time to calculate average calls per second. private DateTime CallsPerSecondStart = DateTime.UtcNow; - /// The number of invocations of this collection. + /// The number of invocations. private long CallCount; - /// The circular buffer which stores all peak invocations - private readonly CircularBuffer PeakInvocations; + /// The peak invocations. + private readonly Stack PeakInvocations; + + /********* + ** Accessors + *********/ /// The associated performance counters. public IDictionary PerformanceCounters { get; } = new Dictionary(); /// The name of this collection. public string Name { get; } - /// Flag if this collection is important (used for the console summary command). - public bool IsImportant { get; } + /// Whether the source is typically invoked at least once per second. + public bool IsPerformanceCritical { get; } /// The alert threshold in milliseconds. public double AlertThresholdMilliseconds { get; set; } - /// If alerting is enabled or not + /// Whether alerts are enabled. public bool EnableAlerts { get; set; } - public PerformanceCounterCollection(PerformanceCounterManager performanceCounterManager, string name, bool isImportant) + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The performance counter manager. + /// The name of this collection. + /// Whether the source is typically invoked at least once per second. + public PerformanceCounterCollection(PerformanceMonitor performanceMonitor, string name, bool isPerformanceCritical = false) { - this.PeakInvocations = new CircularBuffer(PerformanceCounterCollection.MAX_ENTRIES); + this.PeakInvocations = new Stack(this.MaxEntries); this.Name = name; - this.PerformanceCounterManager = performanceCounterManager; - this.IsImportant = isImportant; + this.PerformanceMonitor = performanceMonitor; + this.IsPerformanceCritical = isPerformanceCritical; } - public PerformanceCounterCollection(PerformanceCounterManager performanceCounterManager, string name) - { - this.PeakInvocations = new CircularBuffer(PerformanceCounterCollection.MAX_ENTRIES); - this.PerformanceCounterManager = performanceCounterManager; - this.Name = name; - } - - /// Tracks a single invocation for a named source. + /// Track a single invocation for a named source. /// The name of the source. /// The entry. public void Track(string source, PerformanceCounterEntry entry) { + // add entry if (!this.PerformanceCounters.ContainsKey(source)) this.PerformanceCounters.Add(source, new PerformanceCounter(this, source)); - this.PerformanceCounters[source].Add(entry); + // raise alert if (this.EnableAlerts) this.TriggeredPerformanceCounters.Add(new AlertContext(source, entry.ElapsedMilliseconds)); } - /// Returns the average execution time for all non-game internal sources. - /// The average execution time in milliseconds - public double GetModsAverageExecutionTime() - { - return this.PerformanceCounters.Where(p => - p.Key != Constants.GamePerformanceCounterName).Sum(p => p.Value.GetAverage()); - } - - /// Returns the average execution time for all non-game internal sources. + /// Get the average execution time for all non-game internal sources in milliseconds. /// The interval for which to get the average, relative to now - /// The average execution time in milliseconds public double GetModsAverageExecutionTime(TimeSpan interval) { - return this.PerformanceCounters.Where(p => - p.Key != Constants.GamePerformanceCounterName).Sum(p => p.Value.GetAverage(interval)); - } - - /// Returns the overall average execution time. - /// The average execution time in milliseconds - public double GetAverageExecutionTime() - { - return this.PerformanceCounters.Sum(p => p.Value.GetAverage()); + return this.PerformanceCounters + .Where(entry => entry.Key != Constants.GamePerformanceCounterName) + .Sum(entry => entry.Value.GetAverage(interval)); } - /// Returns the overall average execution time. + /// Get the overall average execution time in milliseconds. /// The interval for which to get the average, relative to now - /// The average execution time in milliseconds public double GetAverageExecutionTime(TimeSpan interval) { - return this.PerformanceCounters.Sum(p => p.Value.GetAverage(interval)); + return this.PerformanceCounters + .Sum(entry => entry.Value.GetAverage(interval)); } - /// Returns the average execution time for game-internal sources. - /// The average execution time in milliseconds - public double GetGameAverageExecutionTime() - { - if (this.PerformanceCounters.TryGetValue(Constants.GamePerformanceCounterName, out PerformanceCounter gameExecTime)) - return gameExecTime.GetAverage(); - - return 0; - } - - /// Returns the average execution time for game-internal sources. - /// The average execution time in milliseconds + /// Get the average execution time for game-internal sources in milliseconds. public double GetGameAverageExecutionTime(TimeSpan interval) { - if (this.PerformanceCounters.TryGetValue(Constants.GamePerformanceCounterName, out PerformanceCounter gameExecTime)) - return gameExecTime.GetAverage(interval); - - return 0; + return this.PerformanceCounters.TryGetValue(Constants.GamePerformanceCounterName, out PerformanceCounter gameExecTime) + ? gameExecTime.GetAverage(interval) + : 0; } - /// Returns the peak execution time - /// The interval for which to get the peak, relative to - /// The DateTime which the is relative to, or DateTime.Now if not given - /// The peak execution time - public double GetPeakExecutionTime(TimeSpan range, DateTime? relativeTo = null) + /// Get the peak execution time in milliseconds. + /// The time range to search. + /// The end time for the , or null for the current time. + public double GetPeakExecutionTime(TimeSpan range, DateTime? endTime = null) { - if (this.PeakInvocations.IsEmpty) + if (this.PeakInvocations.Count == 0) return 0; - if (relativeTo == null) - relativeTo = DateTime.UtcNow; - - DateTime start = relativeTo.Value.Subtract(range); - - var entries = this.PeakInvocations.Where(x => (x.EventTime >= start) && (x.EventTime <= relativeTo)).ToList(); + endTime ??= DateTime.UtcNow; + DateTime startTime = endTime.Value.Subtract(range); - if (!entries.Any()) - return 0; - - return entries.OrderByDescending(x => x.ExecutionTimeMilliseconds).First().ExecutionTimeMilliseconds; + return this.PeakInvocations + .Where(entry => entry.EventTime >= startTime && entry.EventTime <= endTime) + .OrderByDescending(x => x.ExecutionTimeMilliseconds) + .Select(p => p.ExecutionTimeMilliseconds) + .FirstOrDefault(); } - /// Begins tracking the invocation of this collection. + /// Start tracking the invocation of this collection. public void BeginTrackInvocation() { this.TriggeredPerformanceCounters.Clear(); @@ -158,60 +134,58 @@ namespace StardewModdingAPI.Framework.PerformanceCounter this.CallCount++; } - /// Ends tracking the invocation of this collection. Also records an alert if alerting is enabled - /// and the invocation time exceeds the threshold. + /// End tracking the invocation of this collection, and raise an alert if needed. public void EndTrackInvocation() { this.InvocationStopwatch.Stop(); - this.PeakInvocations.Put( - new PeakEntry(this.InvocationStopwatch.Elapsed.TotalMilliseconds, - DateTime.UtcNow, - this.TriggeredPerformanceCounters)); - - if (!this.EnableAlerts) return; + // add invocation + if (this.PeakInvocations.Count >= this.MaxEntries) + this.PeakInvocations.Pop(); + this.PeakInvocations.Push(new PeakEntry(this.InvocationStopwatch.Elapsed.TotalMilliseconds, DateTime.UtcNow, this.TriggeredPerformanceCounters.ToArray())); - if (this.InvocationStopwatch.Elapsed.TotalMilliseconds >= this.AlertThresholdMilliseconds) - this.AddAlert(this.InvocationStopwatch.Elapsed.TotalMilliseconds, - this.AlertThresholdMilliseconds, this.TriggeredPerformanceCounters); + // raise alert + if (this.EnableAlerts && this.InvocationStopwatch.Elapsed.TotalMilliseconds >= this.AlertThresholdMilliseconds) + this.AddAlert(this.InvocationStopwatch.Elapsed.TotalMilliseconds, this.AlertThresholdMilliseconds, this.TriggeredPerformanceCounters.ToArray()); } - /// Adds an alert. + /// Add an alert. /// The execution time in milliseconds. /// The configured threshold. - /// The list of alert contexts. - public void AddAlert(double executionTimeMilliseconds, double thresholdMilliseconds, List alerts) + /// The sources involved in exceeding the threshold. + public void AddAlert(double executionTimeMilliseconds, double thresholdMilliseconds, AlertContext[] alerts) { - this.PerformanceCounterManager.AddAlert(new AlertEntry(this, executionTimeMilliseconds, - thresholdMilliseconds, alerts)); + this.PerformanceMonitor.AddAlert( + new AlertEntry(this, executionTimeMilliseconds, thresholdMilliseconds, alerts) + ); } - /// Adds an alert for a single AlertContext + /// Add an alert. /// The execution time in milliseconds. /// The configured threshold. - /// The context + /// The source involved in exceeding the threshold. public void AddAlert(double executionTimeMilliseconds, double thresholdMilliseconds, AlertContext alert) { - this.AddAlert(executionTimeMilliseconds, thresholdMilliseconds, new List() {alert}); + this.AddAlert(executionTimeMilliseconds, thresholdMilliseconds, new[] { alert }); } - /// Resets the calls per second counter. + /// Reset the calls per second counter. public void ResetCallsPerSecond() { this.CallCount = 0; this.CallsPerSecondStart = DateTime.UtcNow; } - /// Resets all performance counters in this collection. + /// Reset all performance counters in this collection. public void Reset() { this.PeakInvocations.Clear(); - foreach (var i in this.PerformanceCounters) - i.Value.Reset(); + foreach (var counter in this.PerformanceCounters) + counter.Value.Reset(); } - /// Resets the performance counter for a specific source. - /// The source name + /// Reset the performance counter for a specific source. + /// The source name. public void ResetSource(string source) { foreach (var i in this.PerformanceCounters) @@ -219,15 +193,13 @@ namespace StardewModdingAPI.Framework.PerformanceCounter i.Value.Reset(); } - /// Returns the average calls per second. - /// The average calls per second. + /// Get the average calls per second. public long GetAverageCallsPerSecond() { - long runtimeInSeconds = (long) DateTime.UtcNow.Subtract(this.CallsPerSecondStart).TotalSeconds; - - if (runtimeInSeconds == 0) return 0; - - return this.CallCount / runtimeInSeconds; + long runtimeInSeconds = (long)DateTime.UtcNow.Subtract(this.CallsPerSecondStart).TotalSeconds; + return runtimeInSeconds > 0 + ? this.CallCount / runtimeInSeconds + : 0; } } } diff --git a/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterEntry.cs b/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterEntry.cs index a50fce7d..a1d78fc8 100644 --- a/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterEntry.cs +++ b/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterEntry.cs @@ -2,13 +2,29 @@ using System; namespace StardewModdingAPI.Framework.PerformanceCounter { - /// A single performance counter entry. Records the DateTime of the event and the elapsed millisecond. + /// A single performance counter entry. internal struct PerformanceCounterEntry { - /// The DateTime when the entry occured. - public DateTime EventTime; + /********* + ** Accessors + *********/ + /// When the entry occurred. + public DateTime EventTime { get; } - /// The elapsed milliseconds - public double ElapsedMilliseconds; + /// The elapsed milliseconds. + public double ElapsedMilliseconds { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// When the entry occurred. + /// The elapsed milliseconds. + public PerformanceCounterEntry(DateTime eventTime, double elapsedMilliseconds) + { + this.EventTime = eventTime; + this.ElapsedMilliseconds = elapsedMilliseconds; + } } } diff --git a/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterManager.cs b/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterManager.cs index bd964442..81e4e468 100644 --- a/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterManager.cs +++ b/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterManager.cs @@ -7,12 +7,14 @@ using StardewModdingAPI.Framework.Events; namespace StardewModdingAPI.Framework.PerformanceCounter { - internal class PerformanceCounterManager + /// Tracks performance metrics. + internal class PerformanceMonitor { - public HashSet PerformanceCounterCollections = new HashSet(); - + /********* + ** Fields + *********/ /// The recorded alerts. - private readonly List Alerts = new List(); + private readonly IList Alerts = new List(); /// The monitor for output logging. private readonly IMonitor Monitor; @@ -20,62 +22,64 @@ namespace StardewModdingAPI.Framework.PerformanceCounter /// The invocation stopwatch. private readonly Stopwatch InvocationStopwatch = new Stopwatch(); - /// Specifies if alerts should be paused. + /// The underlying performance counter collections. + private readonly IDictionary Collections = new Dictionary(StringComparer.InvariantCultureIgnoreCase); + + + /********* + ** Accessors + *********/ + /// Whether alerts are paused. public bool PauseAlerts { get; set; } - /// Specifies if performance counter tracking should be enabled. + /// Whether performance counter tracking is enabled. public bool EnableTracking { get; set; } - /// Constructs a performance counter manager. + + /********* + ** Public methods + *********/ + /// Construct an instance. /// The monitor for output logging. - public PerformanceCounterManager(IMonitor monitor) + public PerformanceMonitor(IMonitor monitor) { this.Monitor = monitor; } - /// Resets all performance counters in all collections. + /// Reset all performance counters in all collections. public void Reset() { - foreach (PerformanceCounterCollection collection in this.PerformanceCounterCollections) - { + foreach (PerformanceCounterCollection collection in this.Collections.Values) collection.Reset(); - } - - foreach (var eventPerformanceCounter in - this.PerformanceCounterCollections.SelectMany(performanceCounter => performanceCounter.PerformanceCounters)) - { - eventPerformanceCounter.Value.Reset(); - } } - /// Begins tracking the invocation for a collection. - /// The collection name - public void BeginTrackInvocation(string collectionName) + /// Track the invocation time for a collection. + /// The name of the collection. + /// The action to execute and track. + public void Track(string collectionName, Action action) { if (!this.EnableTracking) { + action(); return; } - this.GetOrCreateCollectionByName(collectionName).BeginTrackInvocation(); - } - - /// Ends tracking the invocation for a collection. - /// - public void EndTrackInvocation(string collectionName) - { - if (!this.EnableTracking) + PerformanceCounterCollection collection = this.GetOrCreateCollectionByName(collectionName); + collection.BeginTrackInvocation(); + try { - return; + action(); + } + finally + { + collection.EndTrackInvocation(); } - - this.GetOrCreateCollectionByName(collectionName).EndTrackInvocation(); } - /// Tracks a single performance counter invocation in a specific collection. + /// Track a single performance counter invocation in a specific collection. /// The name of the collection. /// The name of the source. - /// The action to execute and track invocation time for. + /// The action to execute and track. public void Track(string collectionName, string sourceName, Action action) { if (!this.EnableTracking) @@ -84,6 +88,7 @@ namespace StardewModdingAPI.Framework.PerformanceCounter return; } + PerformanceCounterCollection collection = this.GetOrCreateCollectionByName(collectionName); DateTime eventTime = DateTime.UtcNow; this.InvocationStopwatch.Reset(); this.InvocationStopwatch.Start(); @@ -95,79 +100,50 @@ namespace StardewModdingAPI.Framework.PerformanceCounter finally { this.InvocationStopwatch.Stop(); - - this.GetOrCreateCollectionByName(collectionName).Track(sourceName, new PerformanceCounterEntry - { - EventTime = eventTime, - ElapsedMilliseconds = this.InvocationStopwatch.Elapsed.TotalMilliseconds - }); + collection.Track(sourceName, new PerformanceCounterEntry(eventTime, this.InvocationStopwatch.Elapsed.TotalMilliseconds)); } } - /// Gets a collection by name. - /// The name of the collection. - /// The collection or null if none was found. - private PerformanceCounterCollection GetCollectionByName(string name) - { - return this.PerformanceCounterCollections.FirstOrDefault(collection => collection.Name == name); - } - - /// Gets a collection by name and creates it if it doesn't exist. - /// The name of the collection. - /// The collection. - private PerformanceCounterCollection GetOrCreateCollectionByName(string name) - { - PerformanceCounterCollection collection = this.GetCollectionByName(name); - - if (collection != null) return collection; - - collection = new PerformanceCounterCollection(this, name); - this.PerformanceCounterCollections.Add(collection); - - return collection; - } - - /// Resets the performance counters for a specific collection. + /// Reset the performance counters for a specific collection. /// The collection name. public void ResetCollection(string name) { - foreach (PerformanceCounterCollection performanceCounterCollection in - this.PerformanceCounterCollections.Where(performanceCounterCollection => - performanceCounterCollection.Name.Equals(name, StringComparison.InvariantCultureIgnoreCase))) + if (this.Collections.TryGetValue(name, out PerformanceCounterCollection collection)) { - performanceCounterCollection.ResetCallsPerSecond(); - performanceCounterCollection.Reset(); + collection.ResetCallsPerSecond(); + collection.Reset(); } } - /// Resets performance counters for a specific source. + /// Reset performance counters for a specific source. /// The name of the source. public void ResetSource(string name) { - foreach (PerformanceCounterCollection performanceCounterCollection in this.PerformanceCounterCollections) + foreach (PerformanceCounterCollection performanceCounterCollection in this.Collections.Values) performanceCounterCollection.ResetSource(name); } /// Print any queued alerts. public void PrintQueuedAlerts() { - if (this.Alerts.Count == 0) return; + if (this.Alerts.Count == 0) + return; - StringBuilder sb = new StringBuilder(); + StringBuilder report = new StringBuilder(); foreach (AlertEntry alert in this.Alerts) { - sb.AppendLine($"{alert.Collection.Name} took {alert.ExecutionTimeMilliseconds:F2}ms (exceeded threshold of {alert.ThresholdMilliseconds:F2}ms)"); + report.AppendLine($"{alert.Collection.Name} took {alert.ExecutionTimeMilliseconds:F2}ms (exceeded threshold of {alert.ThresholdMilliseconds:F2}ms)"); foreach (AlertContext context in alert.Context.OrderByDescending(p => p.Elapsed)) - sb.AppendLine(context.ToString()); + report.AppendLine(context.ToString()); } this.Alerts.Clear(); - this.Monitor.Log(sb.ToString(), LogLevel.Error); + this.Monitor.Log(report.ToString(), LogLevel.Error); } - /// Adds an alert to the queue. + /// Add an alert to the queue. /// The alert to add. public void AddAlert(AlertEntry entry) { @@ -175,68 +151,34 @@ namespace StardewModdingAPI.Framework.PerformanceCounter this.Alerts.Add(entry); } - /// Initialized the default performance counter collections. + /// Initialize the default performance counter collections. /// The event manager. public void InitializePerformanceCounterCollections(EventManager eventManager) { - this.PerformanceCounterCollections = new HashSet() + foreach (IManagedEvent @event in eventManager.GetAllEvents()) + this.Collections[@event.EventName] = new PerformanceCounterCollection(this, @event.EventName, @event.IsPerformanceCritical); + } + + /// Get the underlying performance counters. + public IEnumerable GetCollections() + { + return this.Collections.Values; + } + + + /********* + ** Public methods + *********/ + /// Get a collection by name and creates it if it doesn't exist. + /// The name of the collection. + private PerformanceCounterCollection GetOrCreateCollectionByName(string name) + { + if (!this.Collections.TryGetValue(name, out PerformanceCounterCollection collection)) { - new EventPerformanceCounterCollection(this, eventManager.MenuChanged, false), - - // Rendering Events - new EventPerformanceCounterCollection(this, eventManager.Rendering, false), - new EventPerformanceCounterCollection(this, eventManager.Rendered, true), - new EventPerformanceCounterCollection(this, eventManager.RenderingWorld, false), - new EventPerformanceCounterCollection(this, eventManager.RenderedWorld, true), - new EventPerformanceCounterCollection(this, eventManager.RenderingActiveMenu, false), - new EventPerformanceCounterCollection(this, eventManager.RenderedActiveMenu, true), - new EventPerformanceCounterCollection(this, eventManager.RenderingHud, false), - new EventPerformanceCounterCollection(this, eventManager.RenderedHud, true), - - new EventPerformanceCounterCollection(this, eventManager.WindowResized, false), - new EventPerformanceCounterCollection(this, eventManager.GameLaunched, false), - new EventPerformanceCounterCollection(this, eventManager.UpdateTicking, true), - new EventPerformanceCounterCollection(this, eventManager.UpdateTicked, true), - new EventPerformanceCounterCollection(this, eventManager.OneSecondUpdateTicking, true), - new EventPerformanceCounterCollection(this, eventManager.OneSecondUpdateTicked, true), - - new EventPerformanceCounterCollection(this, eventManager.SaveCreating, false), - new EventPerformanceCounterCollection(this, eventManager.SaveCreated, false), - new EventPerformanceCounterCollection(this, eventManager.Saving, false), - new EventPerformanceCounterCollection(this, eventManager.Saved, false), - - new EventPerformanceCounterCollection(this, eventManager.DayStarted, false), - new EventPerformanceCounterCollection(this, eventManager.DayEnding, false), - - new EventPerformanceCounterCollection(this, eventManager.TimeChanged, true), - - new EventPerformanceCounterCollection(this, eventManager.ReturnedToTitle, false), - - new EventPerformanceCounterCollection(this, eventManager.ButtonPressed, true), - new EventPerformanceCounterCollection(this, eventManager.ButtonReleased, true), - new EventPerformanceCounterCollection(this, eventManager.CursorMoved, true), - new EventPerformanceCounterCollection(this, eventManager.MouseWheelScrolled, true), - - new EventPerformanceCounterCollection(this, eventManager.PeerContextReceived, false), - new EventPerformanceCounterCollection(this, eventManager.ModMessageReceived, false), - new EventPerformanceCounterCollection(this, eventManager.PeerDisconnected, false), - new EventPerformanceCounterCollection(this, eventManager.InventoryChanged, true), - new EventPerformanceCounterCollection(this, eventManager.LevelChanged, false), - new EventPerformanceCounterCollection(this, eventManager.Warped, false), - - new EventPerformanceCounterCollection(this, eventManager.LocationListChanged, false), - new EventPerformanceCounterCollection(this, eventManager.BuildingListChanged, false), - new EventPerformanceCounterCollection(this, eventManager.LocationListChanged, false), - new EventPerformanceCounterCollection(this, eventManager.DebrisListChanged, true), - new EventPerformanceCounterCollection(this, eventManager.LargeTerrainFeatureListChanged, true), - new EventPerformanceCounterCollection(this, eventManager.NpcListChanged, false), - new EventPerformanceCounterCollection(this, eventManager.ObjectListChanged, true), - new EventPerformanceCounterCollection(this, eventManager.ChestInventoryChanged, true), - new EventPerformanceCounterCollection(this, eventManager.TerrainFeatureListChanged, true), - new EventPerformanceCounterCollection(this, eventManager.LoadStageChanged, false), - new EventPerformanceCounterCollection(this, eventManager.UnvalidatedUpdateTicking, false), - new EventPerformanceCounterCollection(this, eventManager.UnvalidatedUpdateTicked, false), - }; + collection = new PerformanceCounterCollection(this, name); + this.Collections[name] = collection; + } + return collection; } } } diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index af7513e3..ac89587e 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -23,6 +23,7 @@ using StardewModdingAPI.Framework.Models; using StardewModdingAPI.Framework.ModHelpers; using StardewModdingAPI.Framework.ModLoading; using StardewModdingAPI.Framework.Patching; +using StardewModdingAPI.Framework.PerformanceCounter; using StardewModdingAPI.Framework.Reflection; using StardewModdingAPI.Framework.Serialization; using StardewModdingAPI.Patches; @@ -33,7 +34,6 @@ using StardewModdingAPI.Toolkit.Serialization; using StardewModdingAPI.Toolkit.Utilities; using StardewValley; using Object = StardewValley.Object; -using PerformanceCounterManager = StardewModdingAPI.Framework.PerformanceCounter.PerformanceCounterManager; using ThreadState = System.Threading.ThreadState; namespace StardewModdingAPI.Framework @@ -135,8 +135,8 @@ namespace StardewModdingAPI.Framework internal static DeprecationManager DeprecationManager { get; private set; } /// Manages performance counters. - /// This is initialized after the game starts. This is accessed directly because it's not part of the normal class model. - internal static PerformanceCounterManager PerformanceCounterManager { get; private set; } + /// This is initialized after the game starts. This is non-private for use by Console Commands. + internal static PerformanceMonitor PerformanceMonitor { get; private set; } /********* @@ -167,9 +167,9 @@ namespace StardewModdingAPI.Framework }; this.MonitorForGame = this.GetSecondaryMonitor("game"); - SCore.PerformanceCounterManager = new PerformanceCounterManager(this.Monitor); - this.EventManager = new EventManager(this.Monitor, this.ModRegistry, SCore.PerformanceCounterManager); - SCore.PerformanceCounterManager.InitializePerformanceCounterCollections(this.EventManager); + SCore.PerformanceMonitor = new PerformanceMonitor(this.Monitor); + this.EventManager = new EventManager(this.Monitor, this.ModRegistry, SCore.PerformanceMonitor); + SCore.PerformanceMonitor.InitializePerformanceCounterCollections(this.EventManager); SCore.DeprecationManager = new DeprecationManager(this.Monitor, this.ModRegistry); @@ -248,7 +248,7 @@ namespace StardewModdingAPI.Framework jsonHelper: this.Toolkit.JsonHelper, modRegistry: this.ModRegistry, deprecationManager: SCore.DeprecationManager, - performanceCounterManager: SCore.PerformanceCounterManager, + performanceMonitor: SCore.PerformanceMonitor, onGameInitialized: this.InitializeAfterGameStart, onGameExiting: this.Dispose, cancellationToken: this.CancellationToken, @@ -1307,6 +1307,7 @@ namespace StardewModdingAPI.Framework this.ReloadTranslations(this.ModRegistry.GetAll(contentPacks: false)); this.Monitor.Log("Reloaded translation files for all mods. This only affects new translations the mods fetch; if they cached some text, it may not be updated.", LogLevel.Info); break; + default: throw new NotSupportedException($"Unrecognized core SMAPI command '{name}'."); } diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index 266e2e6f..352859ec 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -59,7 +59,8 @@ namespace StardewModdingAPI.Framework /// Manages deprecation warnings. private readonly DeprecationManager DeprecationManager; - private readonly PerformanceCounterManager PerformanceCounterManager; + /// Tracks performance metrics. + private readonly PerformanceMonitor PerformanceMonitor; /// The maximum number of consecutive attempts SMAPI should make to recover from a draw error. private readonly Countdown DrawCrashTimer = new Countdown(60); // 60 ticks = roughly one second @@ -155,12 +156,12 @@ namespace StardewModdingAPI.Framework /// Encapsulates SMAPI's JSON file parsing. /// Tracks the installed mods. /// Manages deprecation warnings. - /// Manages performance monitoring. + /// Tracks performance metrics. /// A callback to invoke after the game finishes initializing. /// A callback to invoke when the game exits. /// Propagates notification that SMAPI should exit. /// Whether to log network traffic. - internal SGame(Monitor monitor, IMonitor monitorForGame, Reflector reflection, Translator translator, EventManager eventManager, JsonHelper jsonHelper, ModRegistry modRegistry, DeprecationManager deprecationManager, PerformanceCounterManager performanceCounterManager, Action onGameInitialized, Action onGameExiting, CancellationTokenSource cancellationToken, bool logNetworkTraffic) + internal SGame(Monitor monitor, IMonitor monitorForGame, Reflector reflection, Translator translator, EventManager eventManager, JsonHelper jsonHelper, ModRegistry modRegistry, DeprecationManager deprecationManager, PerformanceMonitor performanceMonitor, Action onGameInitialized, Action onGameExiting, CancellationTokenSource cancellationToken, bool logNetworkTraffic) { this.OnLoadingFirstAsset = SGame.ConstructorHack.OnLoadingFirstAsset; SGame.ConstructorHack = null; @@ -180,7 +181,7 @@ namespace StardewModdingAPI.Framework this.Reflection = reflection; this.Translator = translator; this.DeprecationManager = deprecationManager; - this.PerformanceCounterManager = performanceCounterManager; + this.PerformanceMonitor = performanceMonitor; this.OnGameInitialized = onGameInitialized; this.OnGameExiting = onGameExiting; Game1.input = new SInputState(); @@ -312,7 +313,7 @@ namespace StardewModdingAPI.Framework try { this.DeprecationManager.PrintQueued(); - this.PerformanceCounterManager.PrintQueuedAlerts(); + this.PerformanceMonitor.PrintQueuedAlerts(); /********* ** First-tick initialization diff --git a/src/SMAPI/Program.cs b/src/SMAPI/Program.cs index 933590d3..c26ae29a 100644 --- a/src/SMAPI/Program.cs +++ b/src/SMAPI/Program.cs @@ -11,7 +11,7 @@ using StardewModdingAPI.Framework; using StardewModdingAPI.Toolkit.Utilities; [assembly: InternalsVisibleTo("SMAPI.Tests")] -[assembly: InternalsVisibleTo("ConsoleCommands")] +[assembly: InternalsVisibleTo("ConsoleCommands")] // for performance monitoring commands [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] // Moq for unit testing namespace StardewModdingAPI { diff --git a/src/SMAPI/SMAPI.csproj b/src/SMAPI/SMAPI.csproj index 0bc290ac..3bb73295 100644 --- a/src/SMAPI/SMAPI.csproj +++ b/src/SMAPI/SMAPI.csproj @@ -15,12 +15,7 @@ icon.ico - - SMAPI_FOR_WINDOWS - - - -- cgit From 910b4a2c4361c429b09bd35fa52d51b24cc17bc2 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 26 Jan 2020 19:52:31 -0500 Subject: tweak namespace --- .../Commands/Other/PerformanceCounterCommand.cs | 2 +- src/SMAPI/Framework/Events/EventManager.cs | 2 +- src/SMAPI/Framework/Events/ManagedEvent.cs | 2 +- .../Framework/PerformanceCounter/AlertContext.cs | 34 ---- .../Framework/PerformanceCounter/AlertEntry.cs | 38 ---- .../Framework/PerformanceCounter/PeakEntry.cs | 35 ---- .../PerformanceCounter/PerformanceCounter.cs | 125 ------------- .../PerformanceCounterCollection.cs | 205 --------------------- .../PerformanceCounter/PerformanceCounterEntry.cs | 30 --- .../PerformanceCounterManager.cs | 184 ------------------ .../PerformanceMonitoring/AlertContext.cs | 34 ++++ .../Framework/PerformanceMonitoring/AlertEntry.cs | 38 ++++ .../Framework/PerformanceMonitoring/PeakEntry.cs | 35 ++++ .../PerformanceMonitoring/PerformanceCounter.cs | 125 +++++++++++++ .../PerformanceCounterCollection.cs | 205 +++++++++++++++++++++ .../PerformanceCounterEntry.cs | 30 +++ .../PerformanceMonitoring/PerformanceMonitor.cs | 184 ++++++++++++++++++ src/SMAPI/Framework/SCore.cs | 2 +- src/SMAPI/Framework/SGame.cs | 2 +- 19 files changed, 656 insertions(+), 656 deletions(-) delete mode 100644 src/SMAPI/Framework/PerformanceCounter/AlertContext.cs delete mode 100644 src/SMAPI/Framework/PerformanceCounter/AlertEntry.cs delete mode 100644 src/SMAPI/Framework/PerformanceCounter/PeakEntry.cs delete mode 100644 src/SMAPI/Framework/PerformanceCounter/PerformanceCounter.cs delete mode 100644 src/SMAPI/Framework/PerformanceCounter/PerformanceCounterCollection.cs delete mode 100644 src/SMAPI/Framework/PerformanceCounter/PerformanceCounterEntry.cs delete mode 100644 src/SMAPI/Framework/PerformanceCounter/PerformanceCounterManager.cs create mode 100644 src/SMAPI/Framework/PerformanceMonitoring/AlertContext.cs create mode 100644 src/SMAPI/Framework/PerformanceMonitoring/AlertEntry.cs create mode 100644 src/SMAPI/Framework/PerformanceMonitoring/PeakEntry.cs create mode 100644 src/SMAPI/Framework/PerformanceMonitoring/PerformanceCounter.cs create mode 100644 src/SMAPI/Framework/PerformanceMonitoring/PerformanceCounterCollection.cs create mode 100644 src/SMAPI/Framework/PerformanceMonitoring/PerformanceCounterEntry.cs create mode 100644 src/SMAPI/Framework/PerformanceMonitoring/PerformanceMonitor.cs (limited to 'src') diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/PerformanceCounterCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/PerformanceCounterCommand.cs index 820f1939..da171a44 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/PerformanceCounterCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/PerformanceCounterCommand.cs @@ -3,7 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Text; using StardewModdingAPI.Framework; -using StardewModdingAPI.Framework.PerformanceCounter; +using StardewModdingAPI.Framework.PerformanceMonitoring; namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other { diff --git a/src/SMAPI/Framework/Events/EventManager.cs b/src/SMAPI/Framework/Events/EventManager.cs index 50dcc9ef..a9dfda97 100644 --- a/src/SMAPI/Framework/Events/EventManager.cs +++ b/src/SMAPI/Framework/Events/EventManager.cs @@ -2,7 +2,7 @@ using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Reflection; using StardewModdingAPI.Events; -using StardewModdingAPI.Framework.PerformanceCounter; +using StardewModdingAPI.Framework.PerformanceMonitoring; namespace StardewModdingAPI.Framework.Events { diff --git a/src/SMAPI/Framework/Events/ManagedEvent.cs b/src/SMAPI/Framework/Events/ManagedEvent.cs index 60e5c599..118b73ac 100644 --- a/src/SMAPI/Framework/Events/ManagedEvent.cs +++ b/src/SMAPI/Framework/Events/ManagedEvent.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; -using StardewModdingAPI.Framework.PerformanceCounter; +using StardewModdingAPI.Framework.PerformanceMonitoring; namespace StardewModdingAPI.Framework.Events { diff --git a/src/SMAPI/Framework/PerformanceCounter/AlertContext.cs b/src/SMAPI/Framework/PerformanceCounter/AlertContext.cs deleted file mode 100644 index 76c472e1..00000000 --- a/src/SMAPI/Framework/PerformanceCounter/AlertContext.cs +++ /dev/null @@ -1,34 +0,0 @@ -namespace StardewModdingAPI.Framework.PerformanceCounter -{ - /// The context for an alert. - internal struct AlertContext - { - /********* - ** Accessors - *********/ - /// The source which triggered the alert. - public string Source { get; } - - /// The elapsed milliseconds. - public double Elapsed { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The source which triggered the alert. - /// The elapsed milliseconds. - public AlertContext(string source, double elapsed) - { - this.Source = source; - this.Elapsed = elapsed; - } - - /// Get a human-readable text form of this instance. - public override string ToString() - { - return $"{this.Source}: {this.Elapsed:F2}ms"; - } - } -} diff --git a/src/SMAPI/Framework/PerformanceCounter/AlertEntry.cs b/src/SMAPI/Framework/PerformanceCounter/AlertEntry.cs deleted file mode 100644 index 494f34a9..00000000 --- a/src/SMAPI/Framework/PerformanceCounter/AlertEntry.cs +++ /dev/null @@ -1,38 +0,0 @@ -namespace StardewModdingAPI.Framework.PerformanceCounter -{ - /// A single alert entry. - internal struct AlertEntry - { - /********* - ** Accessors - *********/ - /// The collection in which the alert occurred. - public PerformanceCounterCollection Collection { get; } - - /// The actual execution time in milliseconds. - public double ExecutionTimeMilliseconds { get; } - - /// The configured alert threshold in milliseconds. - public double ThresholdMilliseconds { get; } - - /// The sources involved in exceeding the threshold. - public AlertContext[] Context { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The collection in which the alert occurred. - /// The actual execution time in milliseconds. - /// The configured alert threshold in milliseconds. - /// The sources involved in exceeding the threshold. - public AlertEntry(PerformanceCounterCollection collection, double executionTimeMilliseconds, double thresholdMilliseconds, AlertContext[] context) - { - this.Collection = collection; - this.ExecutionTimeMilliseconds = executionTimeMilliseconds; - this.ThresholdMilliseconds = thresholdMilliseconds; - this.Context = context; - } - } -} diff --git a/src/SMAPI/Framework/PerformanceCounter/PeakEntry.cs b/src/SMAPI/Framework/PerformanceCounter/PeakEntry.cs deleted file mode 100644 index abb29541..00000000 --- a/src/SMAPI/Framework/PerformanceCounter/PeakEntry.cs +++ /dev/null @@ -1,35 +0,0 @@ -using System; - -namespace StardewModdingAPI.Framework.PerformanceCounter -{ - /// A peak invocation time. - internal struct PeakEntry - { - /********* - ** Accessors - *********/ - /// The actual execution time in milliseconds. - public double ExecutionTimeMilliseconds { get; } - - /// When the entry occurred. - public DateTime EventTime { get; } - - /// The sources involved in exceeding the threshold. - public AlertContext[] Context { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The actual execution time in milliseconds. - /// When the entry occurred. - /// The sources involved in exceeding the threshold. - public PeakEntry(double executionTimeMilliseconds, DateTime eventTime, AlertContext[] context) - { - this.ExecutionTimeMilliseconds = executionTimeMilliseconds; - this.EventTime = eventTime; - this.Context = context; - } - } -} diff --git a/src/SMAPI/Framework/PerformanceCounter/PerformanceCounter.cs b/src/SMAPI/Framework/PerformanceCounter/PerformanceCounter.cs deleted file mode 100644 index 0fb6482d..00000000 --- a/src/SMAPI/Framework/PerformanceCounter/PerformanceCounter.cs +++ /dev/null @@ -1,125 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Harmony; - -namespace StardewModdingAPI.Framework.PerformanceCounter -{ - /// Tracks metadata about a particular code event. - internal class PerformanceCounter - { - /********* - ** Fields - *********/ - /// The size of the ring buffer. - private readonly int MaxEntries = 16384; - - /// The collection to which this performance counter belongs. - private readonly PerformanceCounterCollection ParentCollection; - - /// The performance counter entries. - private readonly Stack Entries; - - /// The entry with the highest execution time. - private PerformanceCounterEntry? PeakPerformanceCounterEntry; - - - /********* - ** Accessors - *********/ - /// The name of the source. - public string Source { get; } - - /// The alert threshold in milliseconds - public double AlertThresholdMilliseconds { get; set; } - - /// If alerting is enabled or not - public bool EnableAlerts { get; set; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The collection to which this performance counter belongs. - /// The name of the source. - public PerformanceCounter(PerformanceCounterCollection parentCollection, string source) - { - this.ParentCollection = parentCollection; - this.Source = source; - this.Entries = new Stack(this.MaxEntries); - } - - /// Add a performance counter entry to the list, update monitoring, and raise alerts if needed. - /// The entry to add. - public void Add(PerformanceCounterEntry entry) - { - // add entry - if (this.Entries.Count > this.MaxEntries) - this.Entries.Pop(); - this.Entries.Add(entry); - - // update metrics - if (this.PeakPerformanceCounterEntry == null || entry.ElapsedMilliseconds > this.PeakPerformanceCounterEntry.Value.ElapsedMilliseconds) - this.PeakPerformanceCounterEntry = entry; - - // raise alert - if (this.EnableAlerts && entry.ElapsedMilliseconds > this.AlertThresholdMilliseconds) - this.ParentCollection.AddAlert(entry.ElapsedMilliseconds, this.AlertThresholdMilliseconds, new AlertContext(this.Source, entry.ElapsedMilliseconds)); - } - - /// Clear all performance counter entries and monitoring. - public void Reset() - { - this.Entries.Clear(); - this.PeakPerformanceCounterEntry = null; - } - - /// Get the peak entry. - public PerformanceCounterEntry? GetPeak() - { - return this.PeakPerformanceCounterEntry; - } - - /// Get the entry with the highest execution time. - /// The time range to search. - /// The end time for the , or null for the current time. - public PerformanceCounterEntry? GetPeak(TimeSpan range, DateTime? endTime = null) - { - endTime ??= DateTime.UtcNow; - DateTime startTime = endTime.Value.Subtract(range); - - return this.Entries - .Where(entry => entry.EventTime >= startTime && entry.EventTime <= endTime) - .OrderByDescending(x => x.ElapsedMilliseconds) - .FirstOrDefault(); - } - - /// Get the last entry added to the list. - public PerformanceCounterEntry? GetLastEntry() - { - if (this.Entries.Count == 0) - return null; - - return this.Entries.Peek(); - } - - /// Get the average over a given time span. - /// The time range to search. - /// The end time for the , or null for the current time. - public double GetAverage(TimeSpan range, DateTime? endTime = null) - { - endTime ??= DateTime.UtcNow; - DateTime startTime = endTime.Value.Subtract(range); - - double[] entries = this.Entries - .Where(entry => entry.EventTime >= startTime && entry.EventTime <= endTime) - .Select(p => p.ElapsedMilliseconds) - .ToArray(); - - return entries.Length > 0 - ? entries.Average() - : 0; - } - } -} diff --git a/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterCollection.cs b/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterCollection.cs deleted file mode 100644 index bd13a36e..00000000 --- a/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterCollection.cs +++ /dev/null @@ -1,205 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; - -namespace StardewModdingAPI.Framework.PerformanceCounter -{ - internal class PerformanceCounterCollection - { - /********* - ** Fields - *********/ - /// The number of peak invocations to keep. - private readonly int MaxEntries = 16384; - - /// The sources involved in exceeding alert thresholds. - private readonly List TriggeredPerformanceCounters = new List(); - - /// The stopwatch used to track the invocation time. - private readonly Stopwatch InvocationStopwatch = new Stopwatch(); - - /// The performance counter manager. - private readonly PerformanceMonitor PerformanceMonitor; - - /// The time to calculate average calls per second. - private DateTime CallsPerSecondStart = DateTime.UtcNow; - - /// The number of invocations. - private long CallCount; - - /// The peak invocations. - private readonly Stack PeakInvocations; - - - /********* - ** Accessors - *********/ - /// The associated performance counters. - public IDictionary PerformanceCounters { get; } = new Dictionary(); - - /// The name of this collection. - public string Name { get; } - - /// Whether the source is typically invoked at least once per second. - public bool IsPerformanceCritical { get; } - - /// The alert threshold in milliseconds. - public double AlertThresholdMilliseconds { get; set; } - - /// Whether alerts are enabled. - public bool EnableAlerts { get; set; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The performance counter manager. - /// The name of this collection. - /// Whether the source is typically invoked at least once per second. - public PerformanceCounterCollection(PerformanceMonitor performanceMonitor, string name, bool isPerformanceCritical = false) - { - this.PeakInvocations = new Stack(this.MaxEntries); - this.Name = name; - this.PerformanceMonitor = performanceMonitor; - this.IsPerformanceCritical = isPerformanceCritical; - } - - /// Track a single invocation for a named source. - /// The name of the source. - /// The entry. - public void Track(string source, PerformanceCounterEntry entry) - { - // add entry - if (!this.PerformanceCounters.ContainsKey(source)) - this.PerformanceCounters.Add(source, new PerformanceCounter(this, source)); - this.PerformanceCounters[source].Add(entry); - - // raise alert - if (this.EnableAlerts) - this.TriggeredPerformanceCounters.Add(new AlertContext(source, entry.ElapsedMilliseconds)); - } - - /// Get the average execution time for all non-game internal sources in milliseconds. - /// The interval for which to get the average, relative to now - public double GetModsAverageExecutionTime(TimeSpan interval) - { - return this.PerformanceCounters - .Where(entry => entry.Key != Constants.GamePerformanceCounterName) - .Sum(entry => entry.Value.GetAverage(interval)); - } - - /// Get the overall average execution time in milliseconds. - /// The interval for which to get the average, relative to now - public double GetAverageExecutionTime(TimeSpan interval) - { - return this.PerformanceCounters - .Sum(entry => entry.Value.GetAverage(interval)); - } - - /// Get the average execution time for game-internal sources in milliseconds. - public double GetGameAverageExecutionTime(TimeSpan interval) - { - return this.PerformanceCounters.TryGetValue(Constants.GamePerformanceCounterName, out PerformanceCounter gameExecTime) - ? gameExecTime.GetAverage(interval) - : 0; - } - - /// Get the peak execution time in milliseconds. - /// The time range to search. - /// The end time for the , or null for the current time. - public double GetPeakExecutionTime(TimeSpan range, DateTime? endTime = null) - { - if (this.PeakInvocations.Count == 0) - return 0; - - endTime ??= DateTime.UtcNow; - DateTime startTime = endTime.Value.Subtract(range); - - return this.PeakInvocations - .Where(entry => entry.EventTime >= startTime && entry.EventTime <= endTime) - .OrderByDescending(x => x.ExecutionTimeMilliseconds) - .Select(p => p.ExecutionTimeMilliseconds) - .FirstOrDefault(); - } - - /// Start tracking the invocation of this collection. - public void BeginTrackInvocation() - { - this.TriggeredPerformanceCounters.Clear(); - this.InvocationStopwatch.Reset(); - this.InvocationStopwatch.Start(); - - this.CallCount++; - } - - /// End tracking the invocation of this collection, and raise an alert if needed. - public void EndTrackInvocation() - { - this.InvocationStopwatch.Stop(); - - // add invocation - if (this.PeakInvocations.Count >= this.MaxEntries) - this.PeakInvocations.Pop(); - this.PeakInvocations.Push(new PeakEntry(this.InvocationStopwatch.Elapsed.TotalMilliseconds, DateTime.UtcNow, this.TriggeredPerformanceCounters.ToArray())); - - // raise alert - if (this.EnableAlerts && this.InvocationStopwatch.Elapsed.TotalMilliseconds >= this.AlertThresholdMilliseconds) - this.AddAlert(this.InvocationStopwatch.Elapsed.TotalMilliseconds, this.AlertThresholdMilliseconds, this.TriggeredPerformanceCounters.ToArray()); - } - - /// Add an alert. - /// The execution time in milliseconds. - /// The configured threshold. - /// The sources involved in exceeding the threshold. - public void AddAlert(double executionTimeMilliseconds, double thresholdMilliseconds, AlertContext[] alerts) - { - this.PerformanceMonitor.AddAlert( - new AlertEntry(this, executionTimeMilliseconds, thresholdMilliseconds, alerts) - ); - } - - /// Add an alert. - /// The execution time in milliseconds. - /// The configured threshold. - /// The source involved in exceeding the threshold. - public void AddAlert(double executionTimeMilliseconds, double thresholdMilliseconds, AlertContext alert) - { - this.AddAlert(executionTimeMilliseconds, thresholdMilliseconds, new[] { alert }); - } - - /// Reset the calls per second counter. - public void ResetCallsPerSecond() - { - this.CallCount = 0; - this.CallsPerSecondStart = DateTime.UtcNow; - } - - /// Reset all performance counters in this collection. - public void Reset() - { - this.PeakInvocations.Clear(); - foreach (var counter in this.PerformanceCounters) - counter.Value.Reset(); - } - - /// Reset the performance counter for a specific source. - /// The source name. - public void ResetSource(string source) - { - foreach (var i in this.PerformanceCounters) - if (i.Value.Source.Equals(source, StringComparison.InvariantCultureIgnoreCase)) - i.Value.Reset(); - } - - /// Get the average calls per second. - public long GetAverageCallsPerSecond() - { - long runtimeInSeconds = (long)DateTime.UtcNow.Subtract(this.CallsPerSecondStart).TotalSeconds; - return runtimeInSeconds > 0 - ? this.CallCount / runtimeInSeconds - : 0; - } - } -} diff --git a/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterEntry.cs b/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterEntry.cs deleted file mode 100644 index a1d78fc8..00000000 --- a/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterEntry.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System; - -namespace StardewModdingAPI.Framework.PerformanceCounter -{ - /// A single performance counter entry. - internal struct PerformanceCounterEntry - { - /********* - ** Accessors - *********/ - /// When the entry occurred. - public DateTime EventTime { get; } - - /// The elapsed milliseconds. - public double ElapsedMilliseconds { get; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// When the entry occurred. - /// The elapsed milliseconds. - public PerformanceCounterEntry(DateTime eventTime, double elapsedMilliseconds) - { - this.EventTime = eventTime; - this.ElapsedMilliseconds = elapsedMilliseconds; - } - } -} diff --git a/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterManager.cs b/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterManager.cs deleted file mode 100644 index 81e4e468..00000000 --- a/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterManager.cs +++ /dev/null @@ -1,184 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Text; -using StardewModdingAPI.Framework.Events; - -namespace StardewModdingAPI.Framework.PerformanceCounter -{ - /// Tracks performance metrics. - internal class PerformanceMonitor - { - /********* - ** Fields - *********/ - /// The recorded alerts. - private readonly IList Alerts = new List(); - - /// The monitor for output logging. - private readonly IMonitor Monitor; - - /// The invocation stopwatch. - private readonly Stopwatch InvocationStopwatch = new Stopwatch(); - - /// The underlying performance counter collections. - private readonly IDictionary Collections = new Dictionary(StringComparer.InvariantCultureIgnoreCase); - - - /********* - ** Accessors - *********/ - /// Whether alerts are paused. - public bool PauseAlerts { get; set; } - - /// Whether performance counter tracking is enabled. - public bool EnableTracking { get; set; } - - - /********* - ** Public methods - *********/ - /// Construct an instance. - /// The monitor for output logging. - public PerformanceMonitor(IMonitor monitor) - { - this.Monitor = monitor; - } - - /// Reset all performance counters in all collections. - public void Reset() - { - foreach (PerformanceCounterCollection collection in this.Collections.Values) - collection.Reset(); - } - - /// Track the invocation time for a collection. - /// The name of the collection. - /// The action to execute and track. - public void Track(string collectionName, Action action) - { - if (!this.EnableTracking) - { - action(); - return; - } - - PerformanceCounterCollection collection = this.GetOrCreateCollectionByName(collectionName); - collection.BeginTrackInvocation(); - try - { - action(); - } - finally - { - collection.EndTrackInvocation(); - } - } - - /// Track a single performance counter invocation in a specific collection. - /// The name of the collection. - /// The name of the source. - /// The action to execute and track. - public void Track(string collectionName, string sourceName, Action action) - { - if (!this.EnableTracking) - { - action(); - return; - } - - PerformanceCounterCollection collection = this.GetOrCreateCollectionByName(collectionName); - DateTime eventTime = DateTime.UtcNow; - this.InvocationStopwatch.Reset(); - this.InvocationStopwatch.Start(); - - try - { - action(); - } - finally - { - this.InvocationStopwatch.Stop(); - collection.Track(sourceName, new PerformanceCounterEntry(eventTime, this.InvocationStopwatch.Elapsed.TotalMilliseconds)); - } - } - - /// Reset the performance counters for a specific collection. - /// The collection name. - public void ResetCollection(string name) - { - if (this.Collections.TryGetValue(name, out PerformanceCounterCollection collection)) - { - collection.ResetCallsPerSecond(); - collection.Reset(); - } - } - - /// Reset performance counters for a specific source. - /// The name of the source. - public void ResetSource(string name) - { - foreach (PerformanceCounterCollection performanceCounterCollection in this.Collections.Values) - performanceCounterCollection.ResetSource(name); - } - - /// Print any queued alerts. - public void PrintQueuedAlerts() - { - if (this.Alerts.Count == 0) - return; - - StringBuilder report = new StringBuilder(); - - foreach (AlertEntry alert in this.Alerts) - { - report.AppendLine($"{alert.Collection.Name} took {alert.ExecutionTimeMilliseconds:F2}ms (exceeded threshold of {alert.ThresholdMilliseconds:F2}ms)"); - - foreach (AlertContext context in alert.Context.OrderByDescending(p => p.Elapsed)) - report.AppendLine(context.ToString()); - } - - this.Alerts.Clear(); - this.Monitor.Log(report.ToString(), LogLevel.Error); - } - - /// Add an alert to the queue. - /// The alert to add. - public void AddAlert(AlertEntry entry) - { - if (!this.PauseAlerts) - this.Alerts.Add(entry); - } - - /// Initialize the default performance counter collections. - /// The event manager. - public void InitializePerformanceCounterCollections(EventManager eventManager) - { - foreach (IManagedEvent @event in eventManager.GetAllEvents()) - this.Collections[@event.EventName] = new PerformanceCounterCollection(this, @event.EventName, @event.IsPerformanceCritical); - } - - /// Get the underlying performance counters. - public IEnumerable GetCollections() - { - return this.Collections.Values; - } - - - /********* - ** Public methods - *********/ - /// Get a collection by name and creates it if it doesn't exist. - /// The name of the collection. - private PerformanceCounterCollection GetOrCreateCollectionByName(string name) - { - if (!this.Collections.TryGetValue(name, out PerformanceCounterCollection collection)) - { - collection = new PerformanceCounterCollection(this, name); - this.Collections[name] = collection; - } - return collection; - } - } -} diff --git a/src/SMAPI/Framework/PerformanceMonitoring/AlertContext.cs b/src/SMAPI/Framework/PerformanceMonitoring/AlertContext.cs new file mode 100644 index 00000000..01197f74 --- /dev/null +++ b/src/SMAPI/Framework/PerformanceMonitoring/AlertContext.cs @@ -0,0 +1,34 @@ +namespace StardewModdingAPI.Framework.PerformanceMonitoring +{ + /// The context for an alert. + internal struct AlertContext + { + /********* + ** Accessors + *********/ + /// The source which triggered the alert. + public string Source { get; } + + /// The elapsed milliseconds. + public double Elapsed { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The source which triggered the alert. + /// The elapsed milliseconds. + public AlertContext(string source, double elapsed) + { + this.Source = source; + this.Elapsed = elapsed; + } + + /// Get a human-readable text form of this instance. + public override string ToString() + { + return $"{this.Source}: {this.Elapsed:F2}ms"; + } + } +} diff --git a/src/SMAPI/Framework/PerformanceMonitoring/AlertEntry.cs b/src/SMAPI/Framework/PerformanceMonitoring/AlertEntry.cs new file mode 100644 index 00000000..f5b80189 --- /dev/null +++ b/src/SMAPI/Framework/PerformanceMonitoring/AlertEntry.cs @@ -0,0 +1,38 @@ +namespace StardewModdingAPI.Framework.PerformanceMonitoring +{ + /// A single alert entry. + internal struct AlertEntry + { + /********* + ** Accessors + *********/ + /// The collection in which the alert occurred. + public PerformanceCounterCollection Collection { get; } + + /// The actual execution time in milliseconds. + public double ExecutionTimeMilliseconds { get; } + + /// The configured alert threshold in milliseconds. + public double ThresholdMilliseconds { get; } + + /// The sources involved in exceeding the threshold. + public AlertContext[] Context { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The collection in which the alert occurred. + /// The actual execution time in milliseconds. + /// The configured alert threshold in milliseconds. + /// The sources involved in exceeding the threshold. + public AlertEntry(PerformanceCounterCollection collection, double executionTimeMilliseconds, double thresholdMilliseconds, AlertContext[] context) + { + this.Collection = collection; + this.ExecutionTimeMilliseconds = executionTimeMilliseconds; + this.ThresholdMilliseconds = thresholdMilliseconds; + this.Context = context; + } + } +} diff --git a/src/SMAPI/Framework/PerformanceMonitoring/PeakEntry.cs b/src/SMAPI/Framework/PerformanceMonitoring/PeakEntry.cs new file mode 100644 index 00000000..cff502ad --- /dev/null +++ b/src/SMAPI/Framework/PerformanceMonitoring/PeakEntry.cs @@ -0,0 +1,35 @@ +using System; + +namespace StardewModdingAPI.Framework.PerformanceMonitoring +{ + /// A peak invocation time. + internal struct PeakEntry + { + /********* + ** Accessors + *********/ + /// The actual execution time in milliseconds. + public double ExecutionTimeMilliseconds { get; } + + /// When the entry occurred. + public DateTime EventTime { get; } + + /// The sources involved in exceeding the threshold. + public AlertContext[] Context { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The actual execution time in milliseconds. + /// When the entry occurred. + /// The sources involved in exceeding the threshold. + public PeakEntry(double executionTimeMilliseconds, DateTime eventTime, AlertContext[] context) + { + this.ExecutionTimeMilliseconds = executionTimeMilliseconds; + this.EventTime = eventTime; + this.Context = context; + } + } +} diff --git a/src/SMAPI/Framework/PerformanceMonitoring/PerformanceCounter.cs b/src/SMAPI/Framework/PerformanceMonitoring/PerformanceCounter.cs new file mode 100644 index 00000000..3cf668ee --- /dev/null +++ b/src/SMAPI/Framework/PerformanceMonitoring/PerformanceCounter.cs @@ -0,0 +1,125 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Harmony; + +namespace StardewModdingAPI.Framework.PerformanceMonitoring +{ + /// Tracks metadata about a particular code event. + internal class PerformanceCounter + { + /********* + ** Fields + *********/ + /// The size of the ring buffer. + private readonly int MaxEntries = 16384; + + /// The collection to which this performance counter belongs. + private readonly PerformanceCounterCollection ParentCollection; + + /// The performance counter entries. + private readonly Stack Entries; + + /// The entry with the highest execution time. + private PerformanceCounterEntry? PeakPerformanceCounterEntry; + + + /********* + ** Accessors + *********/ + /// The name of the source. + public string Source { get; } + + /// The alert threshold in milliseconds + public double AlertThresholdMilliseconds { get; set; } + + /// If alerting is enabled or not + public bool EnableAlerts { get; set; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The collection to which this performance counter belongs. + /// The name of the source. + public PerformanceCounter(PerformanceCounterCollection parentCollection, string source) + { + this.ParentCollection = parentCollection; + this.Source = source; + this.Entries = new Stack(this.MaxEntries); + } + + /// Add a performance counter entry to the list, update monitoring, and raise alerts if needed. + /// The entry to add. + public void Add(PerformanceCounterEntry entry) + { + // add entry + if (this.Entries.Count > this.MaxEntries) + this.Entries.Pop(); + this.Entries.Add(entry); + + // update metrics + if (this.PeakPerformanceCounterEntry == null || entry.ElapsedMilliseconds > this.PeakPerformanceCounterEntry.Value.ElapsedMilliseconds) + this.PeakPerformanceCounterEntry = entry; + + // raise alert + if (this.EnableAlerts && entry.ElapsedMilliseconds > this.AlertThresholdMilliseconds) + this.ParentCollection.AddAlert(entry.ElapsedMilliseconds, this.AlertThresholdMilliseconds, new AlertContext(this.Source, entry.ElapsedMilliseconds)); + } + + /// Clear all performance counter entries and monitoring. + public void Reset() + { + this.Entries.Clear(); + this.PeakPerformanceCounterEntry = null; + } + + /// Get the peak entry. + public PerformanceCounterEntry? GetPeak() + { + return this.PeakPerformanceCounterEntry; + } + + /// Get the entry with the highest execution time. + /// The time range to search. + /// The end time for the , or null for the current time. + public PerformanceCounterEntry? GetPeak(TimeSpan range, DateTime? endTime = null) + { + endTime ??= DateTime.UtcNow; + DateTime startTime = endTime.Value.Subtract(range); + + return this.Entries + .Where(entry => entry.EventTime >= startTime && entry.EventTime <= endTime) + .OrderByDescending(x => x.ElapsedMilliseconds) + .FirstOrDefault(); + } + + /// Get the last entry added to the list. + public PerformanceCounterEntry? GetLastEntry() + { + if (this.Entries.Count == 0) + return null; + + return this.Entries.Peek(); + } + + /// Get the average over a given time span. + /// The time range to search. + /// The end time for the , or null for the current time. + public double GetAverage(TimeSpan range, DateTime? endTime = null) + { + endTime ??= DateTime.UtcNow; + DateTime startTime = endTime.Value.Subtract(range); + + double[] entries = this.Entries + .Where(entry => entry.EventTime >= startTime && entry.EventTime <= endTime) + .Select(p => p.ElapsedMilliseconds) + .ToArray(); + + return entries.Length > 0 + ? entries.Average() + : 0; + } + } +} diff --git a/src/SMAPI/Framework/PerformanceMonitoring/PerformanceCounterCollection.cs b/src/SMAPI/Framework/PerformanceMonitoring/PerformanceCounterCollection.cs new file mode 100644 index 00000000..0bb78c74 --- /dev/null +++ b/src/SMAPI/Framework/PerformanceMonitoring/PerformanceCounterCollection.cs @@ -0,0 +1,205 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; + +namespace StardewModdingAPI.Framework.PerformanceMonitoring +{ + internal class PerformanceCounterCollection + { + /********* + ** Fields + *********/ + /// The number of peak invocations to keep. + private readonly int MaxEntries = 16384; + + /// The sources involved in exceeding alert thresholds. + private readonly List TriggeredPerformanceCounters = new List(); + + /// The stopwatch used to track the invocation time. + private readonly Stopwatch InvocationStopwatch = new Stopwatch(); + + /// The performance counter manager. + private readonly PerformanceMonitor PerformanceMonitor; + + /// The time to calculate average calls per second. + private DateTime CallsPerSecondStart = DateTime.UtcNow; + + /// The number of invocations. + private long CallCount; + + /// The peak invocations. + private readonly Stack PeakInvocations; + + + /********* + ** Accessors + *********/ + /// The associated performance counters. + public IDictionary PerformanceCounters { get; } = new Dictionary(); + + /// The name of this collection. + public string Name { get; } + + /// Whether the source is typically invoked at least once per second. + public bool IsPerformanceCritical { get; } + + /// The alert threshold in milliseconds. + public double AlertThresholdMilliseconds { get; set; } + + /// Whether alerts are enabled. + public bool EnableAlerts { get; set; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The performance counter manager. + /// The name of this collection. + /// Whether the source is typically invoked at least once per second. + public PerformanceCounterCollection(PerformanceMonitor performanceMonitor, string name, bool isPerformanceCritical = false) + { + this.PeakInvocations = new Stack(this.MaxEntries); + this.Name = name; + this.PerformanceMonitor = performanceMonitor; + this.IsPerformanceCritical = isPerformanceCritical; + } + + /// Track a single invocation for a named source. + /// The name of the source. + /// The entry. + public void Track(string source, PerformanceCounterEntry entry) + { + // add entry + if (!this.PerformanceCounters.ContainsKey(source)) + this.PerformanceCounters.Add(source, new PerformanceCounter(this, source)); + this.PerformanceCounters[source].Add(entry); + + // raise alert + if (this.EnableAlerts) + this.TriggeredPerformanceCounters.Add(new AlertContext(source, entry.ElapsedMilliseconds)); + } + + /// Get the average execution time for all non-game internal sources in milliseconds. + /// The interval for which to get the average, relative to now + public double GetModsAverageExecutionTime(TimeSpan interval) + { + return this.PerformanceCounters + .Where(entry => entry.Key != Constants.GamePerformanceCounterName) + .Sum(entry => entry.Value.GetAverage(interval)); + } + + /// Get the overall average execution time in milliseconds. + /// The interval for which to get the average, relative to now + public double GetAverageExecutionTime(TimeSpan interval) + { + return this.PerformanceCounters + .Sum(entry => entry.Value.GetAverage(interval)); + } + + /// Get the average execution time for game-internal sources in milliseconds. + public double GetGameAverageExecutionTime(TimeSpan interval) + { + return this.PerformanceCounters.TryGetValue(Constants.GamePerformanceCounterName, out PerformanceCounter gameExecTime) + ? gameExecTime.GetAverage(interval) + : 0; + } + + /// Get the peak execution time in milliseconds. + /// The time range to search. + /// The end time for the , or null for the current time. + public double GetPeakExecutionTime(TimeSpan range, DateTime? endTime = null) + { + if (this.PeakInvocations.Count == 0) + return 0; + + endTime ??= DateTime.UtcNow; + DateTime startTime = endTime.Value.Subtract(range); + + return this.PeakInvocations + .Where(entry => entry.EventTime >= startTime && entry.EventTime <= endTime) + .OrderByDescending(x => x.ExecutionTimeMilliseconds) + .Select(p => p.ExecutionTimeMilliseconds) + .FirstOrDefault(); + } + + /// Start tracking the invocation of this collection. + public void BeginTrackInvocation() + { + this.TriggeredPerformanceCounters.Clear(); + this.InvocationStopwatch.Reset(); + this.InvocationStopwatch.Start(); + + this.CallCount++; + } + + /// End tracking the invocation of this collection, and raise an alert if needed. + public void EndTrackInvocation() + { + this.InvocationStopwatch.Stop(); + + // add invocation + if (this.PeakInvocations.Count >= this.MaxEntries) + this.PeakInvocations.Pop(); + this.PeakInvocations.Push(new PeakEntry(this.InvocationStopwatch.Elapsed.TotalMilliseconds, DateTime.UtcNow, this.TriggeredPerformanceCounters.ToArray())); + + // raise alert + if (this.EnableAlerts && this.InvocationStopwatch.Elapsed.TotalMilliseconds >= this.AlertThresholdMilliseconds) + this.AddAlert(this.InvocationStopwatch.Elapsed.TotalMilliseconds, this.AlertThresholdMilliseconds, this.TriggeredPerformanceCounters.ToArray()); + } + + /// Add an alert. + /// The execution time in milliseconds. + /// The configured threshold. + /// The sources involved in exceeding the threshold. + public void AddAlert(double executionTimeMilliseconds, double thresholdMilliseconds, AlertContext[] alerts) + { + this.PerformanceMonitor.AddAlert( + new AlertEntry(this, executionTimeMilliseconds, thresholdMilliseconds, alerts) + ); + } + + /// Add an alert. + /// The execution time in milliseconds. + /// The configured threshold. + /// The source involved in exceeding the threshold. + public void AddAlert(double executionTimeMilliseconds, double thresholdMilliseconds, AlertContext alert) + { + this.AddAlert(executionTimeMilliseconds, thresholdMilliseconds, new[] { alert }); + } + + /// Reset the calls per second counter. + public void ResetCallsPerSecond() + { + this.CallCount = 0; + this.CallsPerSecondStart = DateTime.UtcNow; + } + + /// Reset all performance counters in this collection. + public void Reset() + { + this.PeakInvocations.Clear(); + foreach (var counter in this.PerformanceCounters) + counter.Value.Reset(); + } + + /// Reset the performance counter for a specific source. + /// The source name. + public void ResetSource(string source) + { + foreach (var i in this.PerformanceCounters) + if (i.Value.Source.Equals(source, StringComparison.InvariantCultureIgnoreCase)) + i.Value.Reset(); + } + + /// Get the average calls per second. + public long GetAverageCallsPerSecond() + { + long runtimeInSeconds = (long)DateTime.UtcNow.Subtract(this.CallsPerSecondStart).TotalSeconds; + return runtimeInSeconds > 0 + ? this.CallCount / runtimeInSeconds + : 0; + } + } +} diff --git a/src/SMAPI/Framework/PerformanceMonitoring/PerformanceCounterEntry.cs b/src/SMAPI/Framework/PerformanceMonitoring/PerformanceCounterEntry.cs new file mode 100644 index 00000000..8adbd88d --- /dev/null +++ b/src/SMAPI/Framework/PerformanceMonitoring/PerformanceCounterEntry.cs @@ -0,0 +1,30 @@ +using System; + +namespace StardewModdingAPI.Framework.PerformanceMonitoring +{ + /// A single performance counter entry. + internal struct PerformanceCounterEntry + { + /********* + ** Accessors + *********/ + /// When the entry occurred. + public DateTime EventTime { get; } + + /// The elapsed milliseconds. + public double ElapsedMilliseconds { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// When the entry occurred. + /// The elapsed milliseconds. + public PerformanceCounterEntry(DateTime eventTime, double elapsedMilliseconds) + { + this.EventTime = eventTime; + this.ElapsedMilliseconds = elapsedMilliseconds; + } + } +} diff --git a/src/SMAPI/Framework/PerformanceMonitoring/PerformanceMonitor.cs b/src/SMAPI/Framework/PerformanceMonitoring/PerformanceMonitor.cs new file mode 100644 index 00000000..dfc4f31a --- /dev/null +++ b/src/SMAPI/Framework/PerformanceMonitoring/PerformanceMonitor.cs @@ -0,0 +1,184 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using StardewModdingAPI.Framework.Events; + +namespace StardewModdingAPI.Framework.PerformanceMonitoring +{ + /// Tracks performance metrics. + internal class PerformanceMonitor + { + /********* + ** Fields + *********/ + /// The recorded alerts. + private readonly IList Alerts = new List(); + + /// The monitor for output logging. + private readonly IMonitor Monitor; + + /// The invocation stopwatch. + private readonly Stopwatch InvocationStopwatch = new Stopwatch(); + + /// The underlying performance counter collections. + private readonly IDictionary Collections = new Dictionary(StringComparer.InvariantCultureIgnoreCase); + + + /********* + ** Accessors + *********/ + /// Whether alerts are paused. + public bool PauseAlerts { get; set; } + + /// Whether performance counter tracking is enabled. + public bool EnableTracking { get; set; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The monitor for output logging. + public PerformanceMonitor(IMonitor monitor) + { + this.Monitor = monitor; + } + + /// Reset all performance counters in all collections. + public void Reset() + { + foreach (PerformanceCounterCollection collection in this.Collections.Values) + collection.Reset(); + } + + /// Track the invocation time for a collection. + /// The name of the collection. + /// The action to execute and track. + public void Track(string collectionName, Action action) + { + if (!this.EnableTracking) + { + action(); + return; + } + + PerformanceCounterCollection collection = this.GetOrCreateCollectionByName(collectionName); + collection.BeginTrackInvocation(); + try + { + action(); + } + finally + { + collection.EndTrackInvocation(); + } + } + + /// Track a single performance counter invocation in a specific collection. + /// The name of the collection. + /// The name of the source. + /// The action to execute and track. + public void Track(string collectionName, string sourceName, Action action) + { + if (!this.EnableTracking) + { + action(); + return; + } + + PerformanceCounterCollection collection = this.GetOrCreateCollectionByName(collectionName); + DateTime eventTime = DateTime.UtcNow; + this.InvocationStopwatch.Reset(); + this.InvocationStopwatch.Start(); + + try + { + action(); + } + finally + { + this.InvocationStopwatch.Stop(); + collection.Track(sourceName, new PerformanceCounterEntry(eventTime, this.InvocationStopwatch.Elapsed.TotalMilliseconds)); + } + } + + /// Reset the performance counters for a specific collection. + /// The collection name. + public void ResetCollection(string name) + { + if (this.Collections.TryGetValue(name, out PerformanceCounterCollection collection)) + { + collection.ResetCallsPerSecond(); + collection.Reset(); + } + } + + /// Reset performance counters for a specific source. + /// The name of the source. + public void ResetSource(string name) + { + foreach (PerformanceCounterCollection performanceCounterCollection in this.Collections.Values) + performanceCounterCollection.ResetSource(name); + } + + /// Print any queued alerts. + public void PrintQueuedAlerts() + { + if (this.Alerts.Count == 0) + return; + + StringBuilder report = new StringBuilder(); + + foreach (AlertEntry alert in this.Alerts) + { + report.AppendLine($"{alert.Collection.Name} took {alert.ExecutionTimeMilliseconds:F2}ms (exceeded threshold of {alert.ThresholdMilliseconds:F2}ms)"); + + foreach (AlertContext context in alert.Context.OrderByDescending(p => p.Elapsed)) + report.AppendLine(context.ToString()); + } + + this.Alerts.Clear(); + this.Monitor.Log(report.ToString(), LogLevel.Error); + } + + /// Add an alert to the queue. + /// The alert to add. + public void AddAlert(AlertEntry entry) + { + if (!this.PauseAlerts) + this.Alerts.Add(entry); + } + + /// Initialize the default performance counter collections. + /// The event manager. + public void InitializePerformanceCounterCollections(EventManager eventManager) + { + foreach (IManagedEvent @event in eventManager.GetAllEvents()) + this.Collections[@event.EventName] = new PerformanceCounterCollection(this, @event.EventName, @event.IsPerformanceCritical); + } + + /// Get the underlying performance counters. + public IEnumerable GetCollections() + { + return this.Collections.Values; + } + + + /********* + ** Public methods + *********/ + /// Get a collection by name and creates it if it doesn't exist. + /// The name of the collection. + private PerformanceCounterCollection GetOrCreateCollectionByName(string name) + { + if (!this.Collections.TryGetValue(name, out PerformanceCounterCollection collection)) + { + collection = new PerformanceCounterCollection(this, name); + this.Collections[name] = collection; + } + return collection; + } + } +} diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index ac89587e..f996ae97 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -23,7 +23,7 @@ using StardewModdingAPI.Framework.Models; using StardewModdingAPI.Framework.ModHelpers; using StardewModdingAPI.Framework.ModLoading; using StardewModdingAPI.Framework.Patching; -using StardewModdingAPI.Framework.PerformanceCounter; +using StardewModdingAPI.Framework.PerformanceMonitoring; using StardewModdingAPI.Framework.Reflection; using StardewModdingAPI.Framework.Serialization; using StardewModdingAPI.Patches; diff --git a/src/SMAPI/Framework/SGame.cs b/src/SMAPI/Framework/SGame.cs index 352859ec..4b346059 100644 --- a/src/SMAPI/Framework/SGame.cs +++ b/src/SMAPI/Framework/SGame.cs @@ -17,7 +17,7 @@ using StardewModdingAPI.Framework.Content; using StardewModdingAPI.Framework.Events; using StardewModdingAPI.Framework.Input; using StardewModdingAPI.Framework.Networking; -using StardewModdingAPI.Framework.PerformanceCounter; +using StardewModdingAPI.Framework.PerformanceMonitoring; using StardewModdingAPI.Framework.Reflection; using StardewModdingAPI.Framework.StateTracking.Comparers; using StardewModdingAPI.Framework.StateTracking.Snapshots; -- cgit From 805d857e6ee30d422f32ed7da5640b8ac7d562b3 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 26 Jan 2020 20:28:58 -0500 Subject: show warning when using commands while disabled, simplify some commands a bit --- .../Commands/Other/PerformanceCounterCommand.cs | 143 +++++++++------------ 1 file changed, 64 insertions(+), 79 deletions(-) (limited to 'src') diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/PerformanceCounterCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/PerformanceCounterCommand.cs index da171a44..d6e36123 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/PerformanceCounterCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/PerformanceCounterCommand.cs @@ -13,6 +13,9 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other /********* ** Fields *********/ + /// The name of the command. + private const string CommandName = "performance"; + /// The available commands. private enum SubCommand { @@ -31,7 +34,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other *********/ /// Construct an instance. public PerformanceCounterCommand() - : base("performance", PerformanceCounterCommand.GetDescription()) { } + : base(CommandName, PerformanceCounterCommand.GetDescription()) { } /// Handle the command. /// Writes messages to the console and log file. @@ -97,27 +100,13 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other /// The command arguments. private void HandleSummarySubCommand(IMonitor monitor, ArgumentParser args) { - if (!args.TryGet(1, "mode", out string mode, false)) - mode = "important"; + if (!this.AssertEnabled(monitor)) + return; IEnumerable data = SCore.PerformanceMonitor.GetCollections(); - switch (mode) - { - case null: - case "important": - data = data.Where(p => p.IsPerformanceCritical); - break; - - case "all": - break; - - default: - data = data.Where(p => p.Name.ToLowerInvariant().Contains(mode.ToLowerInvariant())); - break; - } double? threshold = null; - if (args.TryGetDecimal(2, "threshold", out decimal t, false)) + if (args.TryGetDecimal(1, "threshold", out decimal t, required: false)) threshold = (double?)t; TimeSpan interval = TimeSpan.FromSeconds(60); @@ -147,23 +136,21 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other /// The command arguments. private void HandleDetailSubCommand(IMonitor monitor, ArgumentParser args) { - var collections = new List(); - TimeSpan averageInterval = TimeSpan.FromSeconds(60); - double? thresholdMilliseconds = null; - string sourceFilter = null; + if (!this.AssertEnabled(monitor)) + return; - if (args.TryGet(1, "collection", out string collectionName)) - { - collections.AddRange(SCore.PerformanceMonitor.GetCollections().Where(collection => collection.Name.ToLowerInvariant().Contains(collectionName.ToLowerInvariant()))); + // parse args + double? thresholdMilliseconds = null; + if (args.TryGetDecimal(1, "threshold", out decimal t, required: false)) + thresholdMilliseconds = (double)t; - if (args.Count >= 2 && decimal.TryParse(args[2], out _) && args.TryGetDecimal(2, "threshold", out decimal value, false)) - thresholdMilliseconds = (double?)value; - else if (args.TryGet(2, "source", out string sourceName, false)) - sourceFilter = sourceName; - } + // get collections + var collections = SCore.PerformanceMonitor.GetCollections(); + // format + TimeSpan averageInterval = TimeSpan.FromSeconds(60); foreach (PerformanceCounterCollection c in collections) - this.OutputPerformanceCollectionDetail(monitor, c, averageInterval, thresholdMilliseconds, sourceFilter); + this.OutputPerformanceCollectionDetail(monitor, c, averageInterval, thresholdMilliseconds); } /// Handles the trigger sub command. @@ -171,6 +158,9 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other /// The command arguments. private void HandleTriggerSubCommand(IMonitor monitor, ArgumentParser args) { + if (!this.AssertEnabled(monitor)) + return; + if (args.TryGet(1, "mode", out string mode, false)) { switch (mode) @@ -210,7 +200,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other break; default: - this.LogUsageError(monitor, $"Unknown mode {mode}. See 'pc help trigger' for usage."); + this.LogUsageError(monitor, $"Unknown mode {mode}. See '{CommandName} help trigger' for usage."); break; } } @@ -331,7 +321,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other if (asDump) { foreach (var item in collectionTriggers) - report.AppendLine($"pc trigger {item.CollectionName} {item.Threshold}"); + report.AppendLine($"{CommandName} trigger {item.CollectionName} {item.Threshold}"); } else { @@ -356,7 +346,7 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other if (asDump) { foreach (SourceTrigger item in sourceTriggers) - report.AppendLine($"pc trigger {item.CollectionName} {item.Threshold} {item.SourceName}"); + report.AppendLine($"{CommandName} trigger {item.CollectionName} {item.Threshold} {item.SourceName}"); } else { @@ -381,6 +371,9 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other /// The command arguments. private void HandleResetSubCommand(IMonitor monitor, ArgumentParser args) { + if (!this.AssertEnabled(monitor)) + return; + if (args.TryGet(1, "type", out string type, false, new[] { "category", "source" })) { args.TryGet(2, "name", out string name); @@ -404,26 +397,17 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other } } - /// Outputs the details for a collection. /// Writes messages to the console and log file. /// The collection. /// The interval over which to calculate the averages. /// The threshold. - /// The source filter. - private void OutputPerformanceCollectionDetail(IMonitor monitor, PerformanceCounterCollection collection, - TimeSpan averageInterval, double? thresholdMilliseconds, string sourceFilter = null) + private void OutputPerformanceCollectionDetail(IMonitor monitor, PerformanceCounterCollection collection, TimeSpan averageInterval, double? thresholdMilliseconds) { StringBuilder report = new StringBuilder($"Performance Counter for {collection.Name}:\n\n"); List> data = collection.PerformanceCounters.ToList(); - if (sourceFilter != null) - { - data = collection.PerformanceCounters.Where(p => - p.Value.Source.ToLowerInvariant().Contains(sourceFilter.ToLowerInvariant())).ToList(); - } - if (thresholdMilliseconds != null) data = data.Where(p => p.Value.GetAverage(averageInterval) >= thresholdMilliseconds).ToList(); @@ -476,46 +460,35 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other switch (subcommand) { case SubCommand.Detail: - report.AppendLine("Usage: pc detail "); - report.AppendLine(" pc detail "); + report.AppendLine($" {CommandName} detail "); report.AppendLine(); report.AppendLine("Displays details for a specific collection."); report.AppendLine(); report.AppendLine("Arguments:"); - report.AppendLine(" Required. The full or partial name of the collection to display."); - report.AppendLine(" Optional. The full or partial name of the source."); report.AppendLine(" Optional. The threshold in milliseconds. Any average execution time below that"); report.AppendLine(" threshold is not reported."); report.AppendLine(); report.AppendLine("Examples:"); - report.AppendLine("pc detail Display.Rendering Displays all performance counters for the 'Display.Rendering' collection"); - report.AppendLine("pc detail Display.Rendering Pathoschild.ChestsAnywhere Displays the 'Display.Rendering' performance counter for 'Pathoschild.ChestsAnywhere'"); - report.AppendLine("pc detail Display.Rendering 5 Displays the 'Display.Rendering' performance counters exceeding an average of 5ms"); + report.AppendLine($"{CommandName} detail 5 Show counters exceeding an average of 5ms"); break; case SubCommand.Summary: - report.AppendLine("Usage: pc summary "); + report.AppendLine($"Usage: {CommandName} summary "); report.AppendLine(); report.AppendLine("Displays the performance counter summary."); report.AppendLine(); report.AppendLine("Arguments:"); - report.AppendLine(" Optional. Defaults to 'important' if omitted. Specifies one of these modes:"); - report.AppendLine(" - all Displays performance counters from all collections"); - report.AppendLine(" - important Displays only important performance counter collections"); - report.AppendLine(); - report.AppendLine(" Optional. Only shows performance counter collections matching the given name"); - report.AppendLine(" Optional. Hides the actual execution time if it is below this threshold"); + report.AppendLine(" Optional. Hides the actual execution time if it's below this threshold"); report.AppendLine(); report.AppendLine("Examples:"); - report.AppendLine("pc summary all Shows all events"); - report.AppendLine("pc summary all 5 Shows all events"); - report.AppendLine("pc summary Display.Rendering Shows only the 'Display.Rendering' collection"); + report.AppendLine($"{CommandName} summary Show all events"); + report.AppendLine($"{CommandName} summary 5 Shows events exceeding an average of 5ms"); break; case SubCommand.Trigger: - report.AppendLine("Usage: pc trigger "); - report.AppendLine("Usage: pc trigger collection "); - report.AppendLine("Usage: pc trigger collection "); + report.AppendLine($"Usage: {CommandName} trigger "); + report.AppendLine($"Usage: {CommandName} trigger collection "); + report.AppendLine($"Usage: {CommandName} trigger collection "); report.AppendLine(); report.AppendLine("Manages alert triggers."); report.AppendLine(); @@ -540,23 +513,23 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other report.AppendLine(); report.AppendLine("Examples:"); report.AppendLine(); - report.AppendLine("pc trigger collection Display.Rendering 10"); + report.AppendLine($"{CommandName} trigger collection Display.Rendering 10"); report.AppendLine(" Sets up an alert trigger which writes on the console if the execution time of all performance counters in"); report.AppendLine(" the 'Display.Rendering' collection exceed 10 milliseconds."); report.AppendLine(); - report.AppendLine("pc trigger collection Display.Rendering 5 Pathoschild.ChestsAnywhere"); + report.AppendLine($"{CommandName} trigger collection Display.Rendering 5 Pathoschild.ChestsAnywhere"); report.AppendLine(" Sets up an alert trigger to write on the console if the execution time of Pathoschild.ChestsAnywhere in"); report.AppendLine(" the 'Display.Rendering' collection exceed 5 milliseconds."); report.AppendLine(); - report.AppendLine("pc trigger collection Display.Rendering 0"); + report.AppendLine($"{CommandName} trigger collection Display.Rendering 0"); report.AppendLine(" Removes the threshold previously defined from the collection. Note that source-specific thresholds are left intact."); report.AppendLine(); - report.AppendLine("pc trigger clear"); + report.AppendLine($"{CommandName} trigger clear"); report.AppendLine(" Clears all previously setup alert triggers."); break; case SubCommand.Reset: - report.AppendLine("Usage: pc reset "); + report.AppendLine($"Usage: {CommandName} reset "); report.AppendLine(); report.AppendLine("Resets performance counters."); report.AppendLine(); @@ -571,9 +544,9 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other report.AppendLine(" or the source. The name must be an exact match."); report.AppendLine(); report.AppendLine("Examples:"); - report.AppendLine("pc reset Resets all performance counters"); - report.AppendLine("pc reset source Pathoschild.ChestsAnywhere Resets all performance for the source named Pathoschild.ChestsAnywhere"); - report.AppendLine("pc reset collection Display.Rendering Resets all performance for the collection named Display.Rendering"); + report.AppendLine($"{CommandName} reset Resets all performance counters"); + report.AppendLine($"{CommandName} reset source Pathoschild.ChestsAnywhere Resets all performance for the source named Pathoschild.ChestsAnywhere"); + report.AppendLine($"{CommandName} reset collection Display.Rendering Resets all performance for the collection named Display.Rendering"); break; } @@ -586,14 +559,12 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other { StringBuilder report = new StringBuilder(); - report.AppendLine("Displays or configures performance monitoring for diagnose issues."); + report.AppendLine("Displays or configures performance monitoring to diagnose issues. Performance monitoring is disabled by default."); report.AppendLine(); - report.AppendLine("A 'performance counter' is a metric which measures execution time across a range of time for a source (e.g. a mod)."); - report.AppendLine("A set of performance counters is organized in a collection to group various areas."); - report.AppendLine("For example, the performance counter collection named 'Display.Rendered' contains one performance"); + report.AppendLine("For example, the counter collection named 'Display.Rendered' contains one performance"); report.AppendLine("counter when the game executes the 'Display.Rendered' event, and another counter for each mod which handles it."); report.AppendLine(); - report.AppendLine("Usage: pc "); + report.AppendLine($"Usage: {CommandName} "); report.AppendLine(); report.AppendLine("Commands:"); report.AppendLine(); @@ -605,8 +576,8 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other report.AppendLine(" disable Disable performance counter recording."); report.AppendLine(" help Show verbose help for the available commands."); report.AppendLine(); - report.AppendLine("To get help for a specific command, use 'pc help ', for example:"); - report.AppendLine("pc help summary"); + report.AppendLine($"To get help for a specific command, use '{CommandName} help ', for example:"); + report.AppendLine($"{CommandName} help summary"); report.AppendLine(); report.AppendLine("Defaults to summary if no command is given."); report.AppendLine(); @@ -614,6 +585,20 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other return report.ToString(); } + /// Log a warning if performance monitoring isn't enabled. + /// Writes messages to the console and log file. + /// Returns whether performance monitoring is enabled. + private bool AssertEnabled(IMonitor monitor) + { + if (!SCore.PerformanceMonitor.EnableTracking) + { + monitor.Log($"Performance monitoring is currently disabled; enter '{CommandName} enable' to enable it.", LogLevel.Warn); + return false; + } + + return true; + } + /********* ** Private models -- cgit From 860b30443ec47ceb271a008c26f3b358cf7bb409 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sun, 26 Jan 2020 20:42:28 -0500 Subject: simplify performance details output --- .../Commands/Other/PerformanceCounterCommand.cs | 85 ++++++++++------------ 1 file changed, 38 insertions(+), 47 deletions(-) (limited to 'src') diff --git a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/PerformanceCounterCommand.cs b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/PerformanceCounterCommand.cs index d6e36123..63851c9d 100644 --- a/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/PerformanceCounterCommand.cs +++ b/src/SMAPI.Mods.ConsoleCommands/Framework/Commands/Other/PerformanceCounterCommand.cs @@ -140,17 +140,47 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other return; // parse args - double? thresholdMilliseconds = null; + double thresholdMilliseconds = 0; if (args.TryGetDecimal(1, "threshold", out decimal t, required: false)) thresholdMilliseconds = (double)t; // get collections var collections = SCore.PerformanceMonitor.GetCollections(); - // format + // render TimeSpan averageInterval = TimeSpan.FromSeconds(60); - foreach (PerformanceCounterCollection c in collections) - this.OutputPerformanceCollectionDetail(monitor, c, averageInterval, thresholdMilliseconds); + StringBuilder report = new StringBuilder($"Showing details for performance counters of {thresholdMilliseconds}+ milliseconds:\n\n"); + bool anyShown = false; + foreach (PerformanceCounterCollection collection in collections) + { + KeyValuePair[] data = collection.PerformanceCounters + .Where(p => p.Value.GetAverage(averageInterval) >= thresholdMilliseconds) + .ToArray(); + + if (data.Any()) + { + anyShown = true; + report.AppendLine($"{collection.Name}:"); + report.AppendLine(this.GetTableString( + data: data, + header: new[] { "Mod", $"Avg Exec Time (last {(int)averageInterval.TotalSeconds}s)", "Last Exec Time", "Peak Exec Time", $"Peak Exec Time (last {(int)averageInterval.TotalSeconds}s)" }, + getRow: item => new[] + { + item.Key, + this.FormatMilliseconds(item.Value.GetAverage(averageInterval), thresholdMilliseconds), + this.FormatMilliseconds(item.Value.GetLastEntry()?.ElapsedMilliseconds), + this.FormatMilliseconds(item.Value.GetPeak()?.ElapsedMilliseconds), + this.FormatMilliseconds(item.Value.GetPeak(averageInterval)?.ElapsedMilliseconds) + }, + true + )); + } + } + + if (!anyShown) + report.AppendLine("No performance counters found."); + + monitor.Log(report.ToString(), LogLevel.Info); } /// Handles the trigger sub command. @@ -397,45 +427,6 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other } } - /// Outputs the details for a collection. - /// Writes messages to the console and log file. - /// The collection. - /// The interval over which to calculate the averages. - /// The threshold. - private void OutputPerformanceCollectionDetail(IMonitor monitor, PerformanceCounterCollection collection, TimeSpan averageInterval, double? thresholdMilliseconds) - { - StringBuilder report = new StringBuilder($"Performance Counter for {collection.Name}:\n\n"); - - List> data = collection.PerformanceCounters.ToList(); - - if (thresholdMilliseconds != null) - data = data.Where(p => p.Value.GetAverage(averageInterval) >= thresholdMilliseconds).ToList(); - - if (data.Any()) - { - report.AppendLine(this.GetTableString( - data: data, - header: new[] { "Mod", $"Avg Exec Time (last {(int)averageInterval.TotalSeconds}s)", "Last Exec Time", "Peak Exec Time", $"Peak Exec Time (last {(int)averageInterval.TotalSeconds}s)" }, - getRow: item => new[] - { - item.Key, - this.FormatMilliseconds(item.Value.GetAverage(averageInterval), thresholdMilliseconds), - this.FormatMilliseconds(item.Value.GetLastEntry()?.ElapsedMilliseconds), - this.FormatMilliseconds(item.Value.GetPeak()?.ElapsedMilliseconds), - this.FormatMilliseconds(item.Value.GetPeak(averageInterval)?.ElapsedMilliseconds) - }, - true - )); - } - else - { - report.Clear(); - report.AppendLine($"Performance Counter for {collection.Name}: none."); - } - - monitor.Log(report.ToString(), LogLevel.Info); - } - /// Formats the given milliseconds value into a string format. Optionally /// allows a threshold to return "-" if the value is less than the threshold. /// The milliseconds to format. Returns "-" if null @@ -443,10 +434,10 @@ namespace StardewModdingAPI.Mods.ConsoleCommands.Framework.Commands.Other /// The formatted milliseconds. private string FormatMilliseconds(double? milliseconds, double? thresholdMilliseconds = null) { - if (milliseconds == null || (thresholdMilliseconds != null && milliseconds < thresholdMilliseconds)) - return "-"; - - return ((double)milliseconds).ToString("F2"); + thresholdMilliseconds ??= 1; + return milliseconds != null && milliseconds >= thresholdMilliseconds + ? ((double)milliseconds).ToString("F2") + : "-"; } /// Shows detailed help for a specific sub command. -- cgit From e5d8acf240f923a09bdaad3fb14b2c34847860dc Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Thu, 30 Jan 2020 22:10:16 -0500 Subject: rework asset editor/loader tracking so they're affected by load order --- docs/release-notes.md | 2 ++ src/SMAPI/Framework/ContentCoordinator.cs | 4 +-- .../ContentManagers/GameContentManager.cs | 36 +++++++--------------- src/SMAPI/Framework/ModLinked.cs | 29 +++++++++++++++++ src/SMAPI/Framework/SCore.cs | 26 +++++++++++++--- 5 files changed, 66 insertions(+), 31 deletions(-) create mode 100644 src/SMAPI/Framework/ModLinked.cs (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index 5379950e..dada7726 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -19,9 +19,11 @@ * For modders: * Asset propagation for player sprites now affects other players' sprites, and updates recolor maps (e.g. sleeves). + * Reworked the order that asset editors/loaders are called between multiple mods to support some framework mods like Content Patcher and Json Assets. Note that the order is undefined and should not be depended on. * Removed invalid-schedule validation which had false positives. * Fixed `helper.Data.Read/WriteGlobalData` using the `Saves` folder. The installer will move existing folders to the appdata folder. * Fixed dialogue asset changes not correctly propagated until the next day. + * Fixed issue where a mod which implemented `IAssetEditor`/`IAssetLoader` on its entry class could then remove itself from the editor/loader list. * For SMAPI/tool developers: * Added internal performance monitoring (thanks to Drachenkätzchen!). This is disabled by default in the current version, but can be enabled using the `performance` console command. diff --git a/src/SMAPI/Framework/ContentCoordinator.cs b/src/SMAPI/Framework/ContentCoordinator.cs index b60483f1..2fd31263 100644 --- a/src/SMAPI/Framework/ContentCoordinator.cs +++ b/src/SMAPI/Framework/ContentCoordinator.cs @@ -65,10 +65,10 @@ namespace StardewModdingAPI.Framework public LocalizedContentManager.LanguageCode Language => this.MainContentManager.Language; /// Interceptors which provide the initial versions of matching assets. - public IDictionary> Loaders { get; } = new Dictionary>(); + public IList> Loaders { get; } = new List>(); /// Interceptors which edit matching assets after they're loaded. - public IDictionary> Editors { get; } = new Dictionary>(); + public IList> Editors { get; } = new List>(); /// The absolute path to the . public string FullRootDirectory { get; } diff --git a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs index 8930267d..eecdda74 100644 --- a/src/SMAPI/Framework/ContentManagers/GameContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/GameContentManager.cs @@ -21,10 +21,10 @@ namespace StardewModdingAPI.Framework.ContentManagers private readonly ContextHash AssetsBeingLoaded = new ContextHash(); /// Interceptors which provide the initial versions of matching assets. - private IDictionary> Loaders => this.Coordinator.Loaders; + private IList> Loaders => this.Coordinator.Loaders; /// Interceptors which edit matching assets after they're loaded. - private IDictionary> Editors => this.Coordinator.Editors; + private IList> Editors => this.Coordinator.Editors; /// A lookup which indicates whether the asset is localizable (i.e. the filename contains the locale), if previously loaded. private readonly IDictionary IsLocalizableLookup; @@ -278,16 +278,16 @@ namespace StardewModdingAPI.Framework.ContentManagers private IAssetData ApplyLoader(IAssetInfo info) { // find matching loaders - var loaders = this.GetInterceptors(this.Loaders) + var loaders = this.Loaders .Where(entry => { try { - return entry.Value.CanLoad(info); + return entry.Data.CanLoad(info); } catch (Exception ex) { - entry.Key.LogAsMod($"Mod failed when checking whether it could load asset '{info.AssetName}', and will be ignored. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); + entry.Mod.LogAsMod($"Mod failed when checking whether it could load asset '{info.AssetName}', and will be ignored. Error details:\n{ex.GetLogSummary()}", LogLevel.Error); return false; } }) @@ -298,14 +298,14 @@ namespace StardewModdingAPI.Framework.ContentManagers return null; if (loaders.Length > 1) { - string[] loaderNames = loaders.Select(p => p.Key.DisplayName).ToArray(); + string[] loaderNames = loaders.Select(p => p.Mod.DisplayName).ToArray(); this.Monitor.Log($"Multiple mods want to provide the '{info.AssetName}' asset ({string.Join(", ", loaderNames)}), but an asset can't be loaded multiple times. SMAPI will use the default asset instead; uninstall one of the mods to fix this. (Message for modders: you should usually use {typeof(IAssetEditor)} instead to avoid conflicts.)", LogLevel.Warn); return null; } // fetch asset from loader - IModMetadata mod = loaders[0].Key; - IAssetLoader loader = loaders[0].Value; + IModMetadata mod = loaders[0].Mod; + IAssetLoader loader = loaders[0].Data; T data; try { @@ -338,11 +338,11 @@ namespace StardewModdingAPI.Framework.ContentManagers IAssetData GetNewData(object data) => new AssetDataForObject(info, data, this.AssertAndNormalizeAssetName); // edit asset - foreach (var entry in this.GetInterceptors(this.Editors)) + foreach (var entry in this.Editors) { // check for match - IModMetadata mod = entry.Key; - IAssetEditor editor = entry.Value; + IModMetadata mod = entry.Mod; + IAssetEditor editor = entry.Data; try { if (!editor.CanEdit(info)) @@ -382,19 +382,5 @@ namespace StardewModdingAPI.Framework.ContentManagers // return result return asset; } - - /// Get all registered interceptors from a list. - private IEnumerable> GetInterceptors(IDictionary> entries) - { - foreach (var entry in entries) - { - IModMetadata mod = entry.Key; - IList interceptors = entry.Value; - - // registered editors - foreach (T interceptor in interceptors) - yield return new KeyValuePair(mod, interceptor); - } - } } } diff --git a/src/SMAPI/Framework/ModLinked.cs b/src/SMAPI/Framework/ModLinked.cs new file mode 100644 index 00000000..8cfe6f5f --- /dev/null +++ b/src/SMAPI/Framework/ModLinked.cs @@ -0,0 +1,29 @@ +namespace StardewModdingAPI.Framework +{ + /// A generic tuple which links something to a mod. + /// The interceptor type. + internal class ModLinked + { + /********* + ** Accessors + *********/ + /// The mod metadata. + public IModMetadata Mod { get; } + + /// The instance linked to the mod. + public T Data { get; } + + + /********* + ** Public methods + *********/ + /// Construct an instance. + /// The mod metadata. + /// The instance linked to the mod. + public ModLinked(IModMetadata mod, T data) + { + this.Mod = mod; + this.Data = data; + } + } +} diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index 9139b371..7e1f8770 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -807,13 +807,13 @@ namespace StardewModdingAPI.Framework { // ReSharper disable SuspiciousTypeConversion.Global if (metadata.Mod is IAssetEditor editor) - helper.ObservableAssetEditors.Add(editor); + this.ContentCore.Editors.Add(new ModLinked(metadata, editor)); if (metadata.Mod is IAssetLoader loader) - helper.ObservableAssetLoaders.Add(loader); + this.ContentCore.Loaders.Add(new ModLinked(metadata, loader)); // ReSharper restore SuspiciousTypeConversion.Global - this.ContentCore.Editors[metadata] = helper.ObservableAssetEditors; - this.ContentCore.Loaders[metadata] = helper.ObservableAssetLoaders; + helper.ObservableAssetEditors.CollectionChanged += (sender, e) => this.OnInterceptorsChanged(metadata, e.NewItems?.Cast(), e.OldItems?.Cast(), this.ContentCore.Editors); + helper.ObservableAssetLoaders.CollectionChanged += (sender, e) => this.OnInterceptorsChanged(metadata, e.NewItems?.Cast(), e.OldItems?.Cast(), this.ContentCore.Loaders); } // call entry method @@ -862,6 +862,24 @@ namespace StardewModdingAPI.Framework this.ModRegistry.AreAllModsInitialized = true; } + /// Handle a mod adding or removing asset interceptors. + /// The asset interceptor type (one of or ). + /// The mod metadata. + /// The interceptors that were added. + /// The interceptors that were removed. + /// The list to update. + private void OnInterceptorsChanged(IModMetadata mod, IEnumerable added, IEnumerable removed, IList> list) + { + foreach (T interceptor in added ?? new T[0]) + list.Add(new ModLinked(mod, interceptor)); + + foreach (T interceptor in removed ?? new T[0]) + { + foreach (ModLinked entry in list.Where(p => p.Mod == mod && object.ReferenceEquals(p.Data, interceptor)).ToArray()) + list.Remove(entry); + } + } + /// Load a given mod. /// The mod to load. /// The mods being loaded. -- cgit From 1a2af714aa4ca3b7dae61e7ee980c23338240129 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 31 Jan 2020 00:16:48 -0500 Subject: fix error message --- src/SMAPI/Framework/SCore.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src') diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index 7e1f8770..44c495ff 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -205,7 +205,7 @@ namespace StardewModdingAPI.Framework #else if (Constants.Platform == Platform.Windows) { - this.Monitor.Log("Oops! You're running {Constants.Platform}, but this version of SMAPI is for Windows. Please reinstall SMAPI to fix this.", LogLevel.Error); + this.Monitor.Log($"Oops! You're running {Constants.Platform}, but this version of SMAPI is for Windows. Please reinstall SMAPI to fix this.", LogLevel.Error); this.PressAnyKeyToExit(); return; } -- cgit From 3f1d7b1d2238d775638b4e471fc626ca26a3849a Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Fri, 31 Jan 2020 22:27:12 -0500 Subject: rework build settings a bit --- build/lib/System.Numerics.dll | Bin 54272 -> 0 bytes build/lib/System.Runtime.Caching.dll | Bin 71168 -> 0 bytes build/prepare-install-package.targets | 88 ++++++++------- src/SMAPI.Installer/README.txt | 44 -------- src/SMAPI.Installer/SMAPI.Installer.csproj | 15 +-- src/SMAPI.Installer/assets/README.txt | 44 ++++++++ src/SMAPI.Installer/assets/System.Numerics.dll | Bin 0 -> 54272 bytes .../assets/System.Runtime.Caching.dll | Bin 0 -> 71168 bytes src/SMAPI.Installer/assets/unix-install.sh | 24 ++++ src/SMAPI.Installer/assets/unix-launcher.sh | 125 +++++++++++++++++++++ src/SMAPI.Installer/assets/windows-exe-config.xml | 5 + src/SMAPI.Installer/assets/windows-install.bat | 8 ++ src/SMAPI.Installer/unix-install.sh | 24 ---- src/SMAPI.Installer/unix-launcher.sh | 125 --------------------- src/SMAPI.Installer/windows-exe-config.xml | 5 - src/SMAPI.Installer/windows-install.bat | 8 -- .../SMAPI.Mods.ConsoleCommands.csproj | 1 - .../SMAPI.Mods.SaveBackup.csproj | 1 - .../SMAPI.Toolkit.CoreInterfaces.csproj | 3 +- src/SMAPI.Toolkit/SMAPI.Toolkit.csproj | 3 +- src/SMAPI/SMAPI.csproj | 3 +- 21 files changed, 256 insertions(+), 270 deletions(-) delete mode 100644 build/lib/System.Numerics.dll delete mode 100644 build/lib/System.Runtime.Caching.dll delete mode 100644 src/SMAPI.Installer/README.txt create mode 100644 src/SMAPI.Installer/assets/README.txt create mode 100644 src/SMAPI.Installer/assets/System.Numerics.dll create mode 100644 src/SMAPI.Installer/assets/System.Runtime.Caching.dll create mode 100644 src/SMAPI.Installer/assets/unix-install.sh create mode 100644 src/SMAPI.Installer/assets/unix-launcher.sh create mode 100644 src/SMAPI.Installer/assets/windows-exe-config.xml create mode 100644 src/SMAPI.Installer/assets/windows-install.bat delete mode 100644 src/SMAPI.Installer/unix-install.sh delete mode 100644 src/SMAPI.Installer/unix-launcher.sh delete mode 100644 src/SMAPI.Installer/windows-exe-config.xml delete mode 100644 src/SMAPI.Installer/windows-install.bat (limited to 'src') diff --git a/build/lib/System.Numerics.dll b/build/lib/System.Numerics.dll deleted file mode 100644 index fed0f92c..00000000 Binary files a/build/lib/System.Numerics.dll and /dev/null differ diff --git a/build/lib/System.Runtime.Caching.dll b/build/lib/System.Runtime.Caching.dll deleted file mode 100644 index a062391d..00000000 Binary files a/build/lib/System.Runtime.Caching.dll and /dev/null differ diff --git a/build/prepare-install-package.targets b/build/prepare-install-package.targets index 61b12039..790b8bad 100644 --- a/build/prepare-install-package.targets +++ b/build/prepare-install-package.targets @@ -7,18 +7,22 @@ --> - $(SolutionDir)\.. - $(RootPath)\bin\$(Configuration) - $(CompiledRootPath)\SMAPI - $(CompiledRootPath)\SMAPI.Toolkit\net4.5 - $(CompiledRootPath)\Mods - $(SolutionDir)\..\bin\SMAPI installer - $(SolutionDir)\..\bin\SMAPI installer for developers windows unix + + $(SolutionDir) + $(SolutionDir)\..\bin + + $(BuildRootPath)\SMAPI\bin\$(Configuration) + $(BuildRootPath)\SMAPI.Toolkit\bin\$(Configuration)\net4.5 + $(BuildRootPath)\SMAPI.Mods.ConsoleCommands\bin\$(Configuration) + $(BuildRootPath)\SMAPI.Mods.SaveBackup\bin\$(Configuration) + + $(OutRootPath)\SMAPI installer + $(OutRootPath)\SMAPI installer for developers - + @@ -26,50 +30,50 @@ - - - - + + + + - + - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + - - - - + + + + - - - - - - + + + + + + - - + + - - + + diff --git a/src/SMAPI.Installer/README.txt b/src/SMAPI.Installer/README.txt deleted file mode 100644 index 0da49a46..00000000 --- a/src/SMAPI.Installer/README.txt +++ /dev/null @@ -1,44 +0,0 @@ - ___ ___ ___ ___ ___ - / /\ /__/\ / /\ / /\ / /\ - / /:/_ | |::\ / /::\ / /::\ / /:/ - / /:/ /\ | |:|:\ / /:/\:\ / /:/\:\ / /:/ - / /:/ /::\ __|__|:|\:\ / /:/~/::\ / /:/~/:/ / /::\ ___ -/__/:/ /:/\:\ /__/::::| \:\ /__/:/ /:/\:\ /__/:/ /:/ /__/:/\:\ /\ -\ \:\/:/~/:/ \ \:\~~\__\/ \ \:\/:/__\/ \ \:\/:/ \__\/ \:\/:/ - \ \::/ /:/ \ \:\ \ \::/ \ \::/ \__\::/ - \__\/ /:/ \ \:\ \ \:\ \ \:\ / /:/ - /__/:/ \ \:\ \ \:\ \ \:\ /__/:/ - \__\/ \__\/ \__\/ \__\/ \__\/ - - -SMAPI lets you run Stardew Valley with mods. Don't forget to download mods separately. - - -Player's guide --------------------------------- -See https://stardewvalleywiki.com/Modding:Player_Guide for help installing SMAPI, adding mods, etc. - - -Manual install --------------------------------- -THIS IS NOT RECOMMENDED FOR MOST PLAYERS. See instructions above instead. -If you really want to install SMAPI manually, here's how. - -1. Unzip "internal/windows-install.dat" (on Windows) or "internal/unix-install.dat" (on Linux/Mac). - You can change '.dat' to '.zip', it's just a normal zip file renamed to prevent confusion. -2. Copy the files from the folder you just unzipped into your game folder. The - `StardewModdingAPI.exe` file should be right next to the game's executable. -3. - - Windows only: if you use Steam, see the install guide above to enable achievements and - overlay. Otherwise, just run StardewModdingAPI.exe in your game folder to play with mods. - - - Linux/Mac only: rename the "StardewValley" file (no extension) to "StardewValley-original", and - "StardewModdingAPI" (no extension) to "StardewValley". Now just launch the game as usual to - play with mods. - -When installing on Linux or Mac: -- Make sure Mono is installed (normally the installer checks for you). While it's not required, - many mods won't work correctly without it. (Specifically, mods which load PNG images may crash or - freeze the game.) -- To configure the color scheme, edit the `smapi-internal/config.json` file and see instructions - there for the 'ColorScheme' setting. diff --git a/src/SMAPI.Installer/SMAPI.Installer.csproj b/src/SMAPI.Installer/SMAPI.Installer.csproj index 3f01c8fe..79e19d89 100644 --- a/src/SMAPI.Installer/SMAPI.Installer.csproj +++ b/src/SMAPI.Installer/SMAPI.Installer.csproj @@ -8,7 +8,6 @@ latest Exe x86 - $(SolutionDir)\..\bin\$(Configuration)\Installer false @@ -17,19 +16,7 @@ - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - + PreserveNewest diff --git a/src/SMAPI.Installer/assets/README.txt b/src/SMAPI.Installer/assets/README.txt new file mode 100644 index 00000000..0da49a46 --- /dev/null +++ b/src/SMAPI.Installer/assets/README.txt @@ -0,0 +1,44 @@ + ___ ___ ___ ___ ___ + / /\ /__/\ / /\ / /\ / /\ + / /:/_ | |::\ / /::\ / /::\ / /:/ + / /:/ /\ | |:|:\ / /:/\:\ / /:/\:\ / /:/ + / /:/ /::\ __|__|:|\:\ / /:/~/::\ / /:/~/:/ / /::\ ___ +/__/:/ /:/\:\ /__/::::| \:\ /__/:/ /:/\:\ /__/:/ /:/ /__/:/\:\ /\ +\ \:\/:/~/:/ \ \:\~~\__\/ \ \:\/:/__\/ \ \:\/:/ \__\/ \:\/:/ + \ \::/ /:/ \ \:\ \ \::/ \ \::/ \__\::/ + \__\/ /:/ \ \:\ \ \:\ \ \:\ / /:/ + /__/:/ \ \:\ \ \:\ \ \:\ /__/:/ + \__\/ \__\/ \__\/ \__\/ \__\/ + + +SMAPI lets you run Stardew Valley with mods. Don't forget to download mods separately. + + +Player's guide +-------------------------------- +See https://stardewvalleywiki.com/Modding:Player_Guide for help installing SMAPI, adding mods, etc. + + +Manual install +-------------------------------- +THIS IS NOT RECOMMENDED FOR MOST PLAYERS. See instructions above instead. +If you really want to install SMAPI manually, here's how. + +1. Unzip "internal/windows-install.dat" (on Windows) or "internal/unix-install.dat" (on Linux/Mac). + You can change '.dat' to '.zip', it's just a normal zip file renamed to prevent confusion. +2. Copy the files from the folder you just unzipped into your game folder. The + `StardewModdingAPI.exe` file should be right next to the game's executable. +3. + - Windows only: if you use Steam, see the install guide above to enable achievements and + overlay. Otherwise, just run StardewModdingAPI.exe in your game folder to play with mods. + + - Linux/Mac only: rename the "StardewValley" file (no extension) to "StardewValley-original", and + "StardewModdingAPI" (no extension) to "StardewValley". Now just launch the game as usual to + play with mods. + +When installing on Linux or Mac: +- Make sure Mono is installed (normally the installer checks for you). While it's not required, + many mods won't work correctly without it. (Specifically, mods which load PNG images may crash or + freeze the game.) +- To configure the color scheme, edit the `smapi-internal/config.json` file and see instructions + there for the 'ColorScheme' setting. diff --git a/src/SMAPI.Installer/assets/System.Numerics.dll b/src/SMAPI.Installer/assets/System.Numerics.dll new file mode 100644 index 00000000..fed0f92c Binary files /dev/null and b/src/SMAPI.Installer/assets/System.Numerics.dll differ diff --git a/src/SMAPI.Installer/assets/System.Runtime.Caching.dll b/src/SMAPI.Installer/assets/System.Runtime.Caching.dll new file mode 100644 index 00000000..a062391d Binary files /dev/null and b/src/SMAPI.Installer/assets/System.Runtime.Caching.dll differ diff --git a/src/SMAPI.Installer/assets/unix-install.sh b/src/SMAPI.Installer/assets/unix-install.sh new file mode 100644 index 00000000..6d0c86ce --- /dev/null +++ b/src/SMAPI.Installer/assets/unix-install.sh @@ -0,0 +1,24 @@ +#!/bin/bash +# Run the SMAPI installer through Mono on Linux or Mac. + +# Move to script's directory +cd "`dirname "$0"`" + +# get cross-distro version of POSIX command +COMMAND="" +if command -v command >/dev/null 2>&1; then + COMMAND="command -v" +elif type type >/dev/null 2>&1; then + COMMAND="type" +fi + +# if $TERM is not set to xterm, mono will bail out when attempting to write to the console. +export TERM=xterm + +# validate Mono & run installer +if $COMMAND mono >/dev/null 2>&1; then + mono internal/unix-install.exe +else + echo "Oops! Looks like Mono isn't installed. Please install Mono from https://mono-project.com, reboot, and run this installer again." + read +fi diff --git a/src/SMAPI.Installer/assets/unix-launcher.sh b/src/SMAPI.Installer/assets/unix-launcher.sh new file mode 100644 index 00000000..b72eed22 --- /dev/null +++ b/src/SMAPI.Installer/assets/unix-launcher.sh @@ -0,0 +1,125 @@ +#!/usr/bin/env bash +# MonoKickstart Shell Script +# Written by Ethan "flibitijibibo" Lee +# Modified for SMAPI by various contributors + +# Move to script's directory +cd "$(dirname "$0")" || exit $? + +# Get the system architecture +UNAME=$(uname) +ARCH=$(uname -m) + +# MonoKickstart picks the right libfolder, so just execute the right binary. +if [ "$UNAME" == "Darwin" ]; then + # ... Except on OSX. + export DYLD_LIBRARY_PATH=$DYLD_LIBRARY_PATH:./osx/ + + # El Capitan is a total idiot and wipes this variable out, making the + # Steam overlay disappear. This sidesteps "System Integrity Protection" + # and resets the variable with Valve's own variable (they provided this + # fix by the way, thanks Valve!). Note that you will need to update your + # launch configuration to the script location, NOT just the app location + # (i.e. Kick.app/Contents/MacOS/Kick, not just Kick.app). + # -flibit + if [ "$STEAM_DYLD_INSERT_LIBRARIES" != "" ] && [ "$DYLD_INSERT_LIBRARIES" == "" ]; then + export DYLD_INSERT_LIBRARIES="$STEAM_DYLD_INSERT_LIBRARIES" + fi + + # this was here before + ln -sf mcs.bin.osx mcs + + # fix "DllNotFoundException: libgdiplus.dylib" errors when loading images in SMAPI + if [ -f libgdiplus.dylib ]; then + rm libgdiplus.dylib + fi + if [ -f /Library/Frameworks/Mono.framework/Versions/Current/lib/libgdiplus.dylib ]; then + ln -s /Library/Frameworks/Mono.framework/Versions/Current/lib/libgdiplus.dylib libgdiplus.dylib + fi + + # launch SMAPI + cp StardewValley.bin.osx StardewModdingAPI.bin.osx + open -a Terminal ./StardewModdingAPI.bin.osx "$@" +else + # choose launcher + LAUNCHER="" + if [ "$ARCH" == "x86_64" ]; then + ln -sf mcs.bin.x86_64 mcs + cp StardewValley.bin.x86_64 StardewModdingAPI.bin.x86_64 + LAUNCHER="./StardewModdingAPI.bin.x86_64 $*" + else + ln -sf mcs.bin.x86 mcs + cp StardewValley.bin.x86 StardewModdingAPI.bin.x86 + LAUNCHER="./StardewModdingAPI.bin.x86 $*" + fi + + # get cross-distro version of POSIX command + COMMAND="" + if command -v command 2>/dev/null; then + COMMAND="command -v" + elif type type 2>/dev/null; then + COMMAND="type" + fi + + # select terminal (prefer xterm for best compatibility, then known supported terminals) + for terminal in xterm gnome-terminal kitty terminator xfce4-terminal konsole terminal termite alacritty mate-terminal x-terminal-emulator; do + if $COMMAND "$terminal" 2>/dev/null; then + # Find the true shell behind x-terminal-emulator + if [ "$(basename "$(readlink -f $(which "$terminal"))")" != "x-terminal-emulator" ]; then + export LAUNCHTERM=$terminal + break; + else + export LAUNCHTERM="$(basename "$(readlink -f $(which x-terminal-emulator))")" + # Remember that we're using x-terminal-emulator just in case it points outside the $PATH + export XTE=1 + break; + fi + fi + done + + # if no terminal was found, run in current shell or with no output + if [ -z "$LAUNCHTERM" ]; then + sh -c 'TERM=xterm $LAUNCHER' + if [ $? -eq 127 ]; then + $LAUNCHER --no-terminal + fi + exit + fi + + # run in selected terminal and account for quirks + case $LAUNCHTERM in + terminator) + # Terminator converts -e to -x when used through x-terminal-emulator for some reason + if $XTE; then + terminator -e "sh -c 'TERM=xterm $LAUNCHER'" + else + terminator -x "sh -c 'TERM=xterm $LAUNCHER'" + fi + ;; + kitty) + # Kitty overrides the TERM varible unless you set it explicitly + kitty -o term=xterm $LAUNCHER + ;; + alacritty) + # Alacritty doesn't like the double quotes or the variable + if [ "$ARCH" == "x86_64" ]; then + alacritty -e sh -c 'TERM=xterm ./StardewModdingAPI.bin.x86_64 $*' + else + alacritty -e sh -c 'TERM=xterm ./StardewModdingAPI.bin.x86 $*' + fi + ;; + xterm|xfce4-terminal|gnome-terminal|terminal|termite|mate-terminal) + $LAUNCHTERM -e "sh -c 'TERM=xterm $LAUNCHER'" + ;; + konsole) + konsole -p Environment=TERM=xterm -e "$LAUNCHER" + ;; + *) + # If we don't know the terminal, just try to run it in the current shell. + sh -c 'TERM=xterm $LAUNCHER' + # if THAT fails, launch with no output + if [ $? -eq 127 ]; then + $LAUNCHER --no-terminal + fi + esac +fi diff --git a/src/SMAPI.Installer/assets/windows-exe-config.xml b/src/SMAPI.Installer/assets/windows-exe-config.xml new file mode 100644 index 00000000..386c7f1a --- /dev/null +++ b/src/SMAPI.Installer/assets/windows-exe-config.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/src/SMAPI.Installer/assets/windows-install.bat b/src/SMAPI.Installer/assets/windows-install.bat new file mode 100644 index 00000000..d02dd4c6 --- /dev/null +++ b/src/SMAPI.Installer/assets/windows-install.bat @@ -0,0 +1,8 @@ +@echo off +echo %~dp0 | findstr /C:"%TEMP%" 1>nul +if not errorlevel 1 ( + echo Oops! It looks like you're running the installer from inside a zip file. Make sure you unzip the download first. + pause +) else ( + start /WAIT /B internal/windows-install.exe +) diff --git a/src/SMAPI.Installer/unix-install.sh b/src/SMAPI.Installer/unix-install.sh deleted file mode 100644 index 6d0c86ce..00000000 --- a/src/SMAPI.Installer/unix-install.sh +++ /dev/null @@ -1,24 +0,0 @@ -#!/bin/bash -# Run the SMAPI installer through Mono on Linux or Mac. - -# Move to script's directory -cd "`dirname "$0"`" - -# get cross-distro version of POSIX command -COMMAND="" -if command -v command >/dev/null 2>&1; then - COMMAND="command -v" -elif type type >/dev/null 2>&1; then - COMMAND="type" -fi - -# if $TERM is not set to xterm, mono will bail out when attempting to write to the console. -export TERM=xterm - -# validate Mono & run installer -if $COMMAND mono >/dev/null 2>&1; then - mono internal/unix-install.exe -else - echo "Oops! Looks like Mono isn't installed. Please install Mono from https://mono-project.com, reboot, and run this installer again." - read -fi diff --git a/src/SMAPI.Installer/unix-launcher.sh b/src/SMAPI.Installer/unix-launcher.sh deleted file mode 100644 index b72eed22..00000000 --- a/src/SMAPI.Installer/unix-launcher.sh +++ /dev/null @@ -1,125 +0,0 @@ -#!/usr/bin/env bash -# MonoKickstart Shell Script -# Written by Ethan "flibitijibibo" Lee -# Modified for SMAPI by various contributors - -# Move to script's directory -cd "$(dirname "$0")" || exit $? - -# Get the system architecture -UNAME=$(uname) -ARCH=$(uname -m) - -# MonoKickstart picks the right libfolder, so just execute the right binary. -if [ "$UNAME" == "Darwin" ]; then - # ... Except on OSX. - export DYLD_LIBRARY_PATH=$DYLD_LIBRARY_PATH:./osx/ - - # El Capitan is a total idiot and wipes this variable out, making the - # Steam overlay disappear. This sidesteps "System Integrity Protection" - # and resets the variable with Valve's own variable (they provided this - # fix by the way, thanks Valve!). Note that you will need to update your - # launch configuration to the script location, NOT just the app location - # (i.e. Kick.app/Contents/MacOS/Kick, not just Kick.app). - # -flibit - if [ "$STEAM_DYLD_INSERT_LIBRARIES" != "" ] && [ "$DYLD_INSERT_LIBRARIES" == "" ]; then - export DYLD_INSERT_LIBRARIES="$STEAM_DYLD_INSERT_LIBRARIES" - fi - - # this was here before - ln -sf mcs.bin.osx mcs - - # fix "DllNotFoundException: libgdiplus.dylib" errors when loading images in SMAPI - if [ -f libgdiplus.dylib ]; then - rm libgdiplus.dylib - fi - if [ -f /Library/Frameworks/Mono.framework/Versions/Current/lib/libgdiplus.dylib ]; then - ln -s /Library/Frameworks/Mono.framework/Versions/Current/lib/libgdiplus.dylib libgdiplus.dylib - fi - - # launch SMAPI - cp StardewValley.bin.osx StardewModdingAPI.bin.osx - open -a Terminal ./StardewModdingAPI.bin.osx "$@" -else - # choose launcher - LAUNCHER="" - if [ "$ARCH" == "x86_64" ]; then - ln -sf mcs.bin.x86_64 mcs - cp StardewValley.bin.x86_64 StardewModdingAPI.bin.x86_64 - LAUNCHER="./StardewModdingAPI.bin.x86_64 $*" - else - ln -sf mcs.bin.x86 mcs - cp StardewValley.bin.x86 StardewModdingAPI.bin.x86 - LAUNCHER="./StardewModdingAPI.bin.x86 $*" - fi - - # get cross-distro version of POSIX command - COMMAND="" - if command -v command 2>/dev/null; then - COMMAND="command -v" - elif type type 2>/dev/null; then - COMMAND="type" - fi - - # select terminal (prefer xterm for best compatibility, then known supported terminals) - for terminal in xterm gnome-terminal kitty terminator xfce4-terminal konsole terminal termite alacritty mate-terminal x-terminal-emulator; do - if $COMMAND "$terminal" 2>/dev/null; then - # Find the true shell behind x-terminal-emulator - if [ "$(basename "$(readlink -f $(which "$terminal"))")" != "x-terminal-emulator" ]; then - export LAUNCHTERM=$terminal - break; - else - export LAUNCHTERM="$(basename "$(readlink -f $(which x-terminal-emulator))")" - # Remember that we're using x-terminal-emulator just in case it points outside the $PATH - export XTE=1 - break; - fi - fi - done - - # if no terminal was found, run in current shell or with no output - if [ -z "$LAUNCHTERM" ]; then - sh -c 'TERM=xterm $LAUNCHER' - if [ $? -eq 127 ]; then - $LAUNCHER --no-terminal - fi - exit - fi - - # run in selected terminal and account for quirks - case $LAUNCHTERM in - terminator) - # Terminator converts -e to -x when used through x-terminal-emulator for some reason - if $XTE; then - terminator -e "sh -c 'TERM=xterm $LAUNCHER'" - else - terminator -x "sh -c 'TERM=xterm $LAUNCHER'" - fi - ;; - kitty) - # Kitty overrides the TERM varible unless you set it explicitly - kitty -o term=xterm $LAUNCHER - ;; - alacritty) - # Alacritty doesn't like the double quotes or the variable - if [ "$ARCH" == "x86_64" ]; then - alacritty -e sh -c 'TERM=xterm ./StardewModdingAPI.bin.x86_64 $*' - else - alacritty -e sh -c 'TERM=xterm ./StardewModdingAPI.bin.x86 $*' - fi - ;; - xterm|xfce4-terminal|gnome-terminal|terminal|termite|mate-terminal) - $LAUNCHTERM -e "sh -c 'TERM=xterm $LAUNCHER'" - ;; - konsole) - konsole -p Environment=TERM=xterm -e "$LAUNCHER" - ;; - *) - # If we don't know the terminal, just try to run it in the current shell. - sh -c 'TERM=xterm $LAUNCHER' - # if THAT fails, launch with no output - if [ $? -eq 127 ]; then - $LAUNCHER --no-terminal - fi - esac -fi diff --git a/src/SMAPI.Installer/windows-exe-config.xml b/src/SMAPI.Installer/windows-exe-config.xml deleted file mode 100644 index 386c7f1a..00000000 --- a/src/SMAPI.Installer/windows-exe-config.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/src/SMAPI.Installer/windows-install.bat b/src/SMAPI.Installer/windows-install.bat deleted file mode 100644 index d02dd4c6..00000000 --- a/src/SMAPI.Installer/windows-install.bat +++ /dev/null @@ -1,8 +0,0 @@ -@echo off -echo %~dp0 | findstr /C:"%TEMP%" 1>nul -if not errorlevel 1 ( - echo Oops! It looks like you're running the installer from inside a zip file. Make sure you unzip the download first. - pause -) else ( - start /WAIT /B internal/windows-install.exe -) diff --git a/src/SMAPI.Mods.ConsoleCommands/SMAPI.Mods.ConsoleCommands.csproj b/src/SMAPI.Mods.ConsoleCommands/SMAPI.Mods.ConsoleCommands.csproj index ce35bf73..526d406b 100644 --- a/src/SMAPI.Mods.ConsoleCommands/SMAPI.Mods.ConsoleCommands.csproj +++ b/src/SMAPI.Mods.ConsoleCommands/SMAPI.Mods.ConsoleCommands.csproj @@ -5,7 +5,6 @@ StardewModdingAPI.Mods.ConsoleCommands net45 latest - $(SolutionDir)\..\bin\$(Configuration)\Mods\ConsoleCommands false x86 diff --git a/src/SMAPI.Mods.SaveBackup/SMAPI.Mods.SaveBackup.csproj b/src/SMAPI.Mods.SaveBackup/SMAPI.Mods.SaveBackup.csproj index 2d031408..970ccea8 100644 --- a/src/SMAPI.Mods.SaveBackup/SMAPI.Mods.SaveBackup.csproj +++ b/src/SMAPI.Mods.SaveBackup/SMAPI.Mods.SaveBackup.csproj @@ -5,7 +5,6 @@ StardewModdingAPI.Mods.SaveBackup net45 latest - $(SolutionDir)\..\bin\$(Configuration)\Mods\SaveBackup false x86 diff --git a/src/SMAPI.Toolkit.CoreInterfaces/SMAPI.Toolkit.CoreInterfaces.csproj b/src/SMAPI.Toolkit.CoreInterfaces/SMAPI.Toolkit.CoreInterfaces.csproj index 1b9c04ff..accc9175 100644 --- a/src/SMAPI.Toolkit.CoreInterfaces/SMAPI.Toolkit.CoreInterfaces.csproj +++ b/src/SMAPI.Toolkit.CoreInterfaces/SMAPI.Toolkit.CoreInterfaces.csproj @@ -5,9 +5,8 @@ StardewModdingAPI Provides toolkit interfaces which are available to SMAPI mods. net4.5;netstandard2.0 - ..\..\bin\$(Configuration)\SMAPI.Toolkit.CoreInterfaces - ..\..\bin\$(Configuration)\SMAPI.Toolkit.CoreInterfaces\$(TargetFramework)\SMAPI.Toolkit.CoreInterfaces.xml latest + bin\$(Configuration)\$(TargetFramework)\SMAPI.Toolkit.CoreInterfaces.xml x86 diff --git a/src/SMAPI.Toolkit/SMAPI.Toolkit.csproj b/src/SMAPI.Toolkit/SMAPI.Toolkit.csproj index 3bb7e313..eaf8510b 100644 --- a/src/SMAPI.Toolkit/SMAPI.Toolkit.csproj +++ b/src/SMAPI.Toolkit/SMAPI.Toolkit.csproj @@ -5,9 +5,8 @@ StardewModdingAPI.Toolkit A library which encapsulates mod-handling logic for mod managers and tools. Not intended for use by mods. net4.5;netstandard2.0 - ..\..\bin\$(Configuration)\SMAPI.Toolkit - ..\..\bin\$(Configuration)\SMAPI.Toolkit\$(TargetFramework)\SMAPI.Toolkit.xml latest + bin\$(Configuration)\$(TargetFramework)\SMAPI.Toolkit.xml x86 StardewModdingAPI.Toolkit diff --git a/src/SMAPI/SMAPI.csproj b/src/SMAPI/SMAPI.csproj index 3bb73295..1376c8a2 100644 --- a/src/SMAPI/SMAPI.csproj +++ b/src/SMAPI/SMAPI.csproj @@ -8,8 +8,7 @@ latest x86 Exe - $(SolutionDir)\..\bin\$(Configuration)\SMAPI - $(SolutionDir)\..\bin\$(Configuration)\SMAPI\StardewModdingAPI.xml + bin\$(Configuration)\StardewModdingAPI.xml false true icon.ico -- cgit From 125b38c6e62d86115045c2cece6c6c3d1da35600 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 1 Feb 2020 00:18:56 -0500 Subject: improve Save Backup compatibility on Android --- docs/release-notes.md | 4 +++ src/SMAPI.Mods.SaveBackup/ModEntry.cs | 50 ++++++++++++++++++++++++----------- 2 files changed, 39 insertions(+), 15 deletions(-) (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index dada7726..b49307c6 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -17,6 +17,10 @@ * Added `performance` command to track mod performance metrics. This is an advanced experimental feature. (Thanks to Drachenkätzchen!) * Added `test_input` command to view button codes in the console. +* For the Save Backup mod: + * Fixed extra files under `Saves` (e.g. manual backups) not being ignored. + * Fixed Android issue where game files were backed up. + * For modders: * Asset propagation for player sprites now affects other players' sprites, and updates recolor maps (e.g. sleeves). * Reworked the order that asset editors/loaders are called between multiple mods to support some framework mods like Content Patcher and Json Assets. Note that the order is undefined and should not be depended on. diff --git a/src/SMAPI.Mods.SaveBackup/ModEntry.cs b/src/SMAPI.Mods.SaveBackup/ModEntry.cs index 3b47759b..b1d368a1 100644 --- a/src/SMAPI.Mods.SaveBackup/ModEntry.cs +++ b/src/SMAPI.Mods.SaveBackup/ModEntry.cs @@ -68,22 +68,21 @@ namespace StardewModdingAPI.Mods.SaveBackup if (targetFile.Exists || fallbackDir.Exists) return; - // back up saves - this.Monitor.Log($"Backing up saves to {targetFile.FullName}...", LogLevel.Trace); - if (!this.TryCompress(Constants.SavesPath, targetFile, out Exception compressError)) + // copy saves to fallback directory (ignore non-save files/folders) + this.Monitor.Log($"Backing up saves to {fallbackDir.FullName}...", LogLevel.Trace); + DirectoryInfo savesDir = new DirectoryInfo(Constants.SavesPath); + this.RecursiveCopy(savesDir, fallbackDir, copyRoot: false, entry => this.MatchSaveFolders(savesDir, entry)); + + // compress backup if possible + this.Monitor.Log("Compressing backup if possible...", LogLevel.Trace); + if (!this.TryCompress(fallbackDir.FullName, targetFile, out Exception compressError)) { - // log error (expected on Android due to missing compression DLLs) - if (Constants.TargetPlatform == GamePlatform.Android) - this.Monitor.VerboseLog($"Compression isn't supported on Android:\n{compressError}"); - else - { - this.Monitor.Log("Couldn't zip the save backup, creating uncompressed backup instead.", LogLevel.Debug); - this.Monitor.Log(compressError.ToString(), LogLevel.Trace); - } - - // fallback to uncompressed - this.RecursiveCopy(new DirectoryInfo(Constants.SavesPath), fallbackDir, copyRoot: false); + if (Constants.TargetPlatform != GamePlatform.Android) // expected to fail on Android + this.Monitor.Log($"Couldn't compress backup, leaving it uncompressed.\n{compressError}", LogLevel.Trace); } + else + fallbackDir.Delete(recursive: true); + this.Monitor.Log("Backup done!", LogLevel.Trace); } catch (Exception ex) @@ -198,12 +197,16 @@ namespace StardewModdingAPI.Mods.SaveBackup /// The file or folder to copy. /// The folder to copy into. /// Whether to copy the root folder itself, or false to only copy its contents. + /// A filter which matches the files or directories to copy, or null to copy everything. /// Derived from the SMAPI installer code. - private void RecursiveCopy(FileSystemInfo source, DirectoryInfo targetFolder, bool copyRoot = true) + private void RecursiveCopy(FileSystemInfo source, DirectoryInfo targetFolder, bool copyRoot = true, Func filter = null) { if (!targetFolder.Exists) targetFolder.Create(); + if (filter?.Invoke(source) == false) + return; + switch (source) { case FileInfo sourceFile: @@ -220,5 +223,22 @@ namespace StardewModdingAPI.Mods.SaveBackup throw new NotSupportedException($"Unknown filesystem info type '{source.GetType().FullName}'."); } } + + /// A copy filter which matches save folders. + /// The folder containing save folders. + /// The current entry to check under . + private bool MatchSaveFolders(DirectoryInfo savesFolder, FileSystemInfo entry) + { + // only need to filter top-level entries + string parentPath = (entry as FileInfo)?.DirectoryName ?? (entry as DirectoryInfo)?.Parent?.FullName; + if (parentPath != savesFolder.FullName) + return true; + + + // match folders with Name_ID format + return + entry is DirectoryInfo + && ulong.TryParse(entry.Name.Split('_').Last(), out _); + } } } -- cgit From c8191449a00e3db08214e3b1146e17f89f0245c5 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 1 Feb 2020 00:27:53 -0500 Subject: add support for *.tmx map files --- build/common.targets | 3 ++- build/prepare-install-package.targets | 1 + docs/release-notes.md | 1 + .../Framework/ContentManagers/ModContentManager.cs | 1 + src/SMAPI/Framework/SCore.cs | 13 ++++++++++- src/SMAPI/SMAPI.csproj | 27 ++-------------------- 6 files changed, 19 insertions(+), 27 deletions(-) (limited to 'src') diff --git a/build/common.targets b/build/common.targets index df2d4861..cfdcccca 100644 --- a/build/common.targets +++ b/build/common.targets @@ -31,8 +31,9 @@ - + + diff --git a/build/prepare-install-package.targets b/build/prepare-install-package.targets index 790b8bad..7b9d63f9 100644 --- a/build/prepare-install-package.targets +++ b/build/prepare-install-package.targets @@ -45,6 +45,7 @@ + diff --git a/docs/release-notes.md b/docs/release-notes.md index b49307c6..1ad73492 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -22,6 +22,7 @@ * Fixed Android issue where game files were backed up. * For modders: + * Added support for loading `.tmx` map files. * Asset propagation for player sprites now affects other players' sprites, and updates recolor maps (e.g. sleeves). * Reworked the order that asset editors/loaders are called between multiple mods to support some framework mods like Content Patcher and Json Assets. Note that the order is undefined and should not be depended on. * Removed invalid-schedule validation which had false positives. diff --git a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs index fdf76b24..0a526fc8 100644 --- a/src/SMAPI/Framework/ContentManagers/ModContentManager.cs +++ b/src/SMAPI/Framework/ContentManagers/ModContentManager.cs @@ -154,6 +154,7 @@ namespace StardewModdingAPI.Framework.ContentManagers // unpacked map case ".tbin": + case ".tmx": { // validate if (typeof(T) != typeof(Map)) diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index 44c495ff..77c2fab8 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -234,7 +234,7 @@ namespace StardewModdingAPI.Framework #endif AppDomain.CurrentDomain.UnhandledException += (sender, e) => this.Monitor.Log($"Critical app domain exception: {e.ExceptionObject}", LogLevel.Error); - // add more lenient assembly resolvers + // add more lenient assembly resolver AppDomain.CurrentDomain.AssemblyResolve += (sender, e) => AssemblyLoader.ResolveAssembly(e.Name); // hook locale event @@ -420,6 +420,17 @@ namespace StardewModdingAPI.Framework return; } + // init TMX support + try + { + xTile.Format.FormatManager.Instance.RegisterMapFormat(new TMXTile.TMXFormat(Game1.tileSize / Game1.pixelZoom, Game1.tileSize / Game1.pixelZoom, Game1.pixelZoom, Game1.pixelZoom)); + } + catch (Exception ex) + { + this.Monitor.Log("SMAPI couldn't load TMX support. Some mods may not work correctly.", LogLevel.Warn); + this.Monitor.Log($"Technical details: {ex.GetLogSummary()}", LogLevel.Trace); + } + // load mod data ModToolkit toolkit = new ModToolkit(); ModDatabase modDatabase = toolkit.GetModDatabase(Constants.ApiMetadataPath); diff --git a/src/SMAPI/SMAPI.csproj b/src/SMAPI/SMAPI.csproj index 1376c8a2..579af423 100644 --- a/src/SMAPI/SMAPI.csproj +++ b/src/SMAPI/SMAPI.csproj @@ -19,6 +19,7 @@ + @@ -98,31 +99,7 @@ SMAPI.metadata.json PreserveNewest - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - - PreserveNewest - - + PreserveNewest -- cgit From 70a1334f2c50cff279344b9b9d52d71c847516a7 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 1 Feb 2020 01:08:29 -0500 Subject: add JSON converter for Vector2 --- docs/release-notes.md | 1 + src/SMAPI/Framework/SCore.cs | 1 + .../Framework/Serialization/PointConverter.cs | 2 +- .../Framework/Serialization/Vector2Converter.cs | 43 ++++++++++++++++++++++ 4 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 src/SMAPI/Framework/Serialization/Vector2Converter.cs (limited to 'src') diff --git a/docs/release-notes.md b/docs/release-notes.md index 1ad73492..13735e76 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -23,6 +23,7 @@ * For modders: * Added support for loading `.tmx` map files. + * Added JSON converter for `Vector2` values, so they work consistently crossplatform. * Asset propagation for player sprites now affects other players' sprites, and updates recolor maps (e.g. sleeves). * Reworked the order that asset editors/loaders are called between multiple mods to support some framework mods like Content Patcher and Json Assets. Note that the order is undefined and should not be depended on. * Removed invalid-schedule validation which had false positives. diff --git a/src/SMAPI/Framework/SCore.cs b/src/SMAPI/Framework/SCore.cs index 77c2fab8..50e6ea1c 100644 --- a/src/SMAPI/Framework/SCore.cs +++ b/src/SMAPI/Framework/SCore.cs @@ -222,6 +222,7 @@ namespace StardewModdingAPI.Framework JsonConverter[] converters = { new ColorConverter(), new PointConverter(), + new Vector2Converter(), new RectangleConverter() }; foreach (JsonConverter converter in converters) diff --git a/src/SMAPI/Framework/Serialization/PointConverter.cs b/src/SMAPI/Framework/Serialization/PointConverter.cs index 8c2f3396..3481c9b2 100644 --- a/src/SMAPI/Framework/Serialization/PointConverter.cs +++ b/src/SMAPI/Framework/Serialization/PointConverter.cs @@ -6,7 +6,7 @@ using StardewModdingAPI.Toolkit.Serialization.Converters; namespace StardewModdingAPI.Framework.Serialization { - /// Handles deserialization of for crossplatform compatibility. + /// Handles deserialization of for crossplatform compatibility. /// /// - Linux/Mac format: { "X": 1, "Y": 2 } /// - Windows format: "1, 2" diff --git a/src/SMAPI/Framework/Serialization/Vector2Converter.cs b/src/SMAPI/Framework/Serialization/Vector2Converter.cs new file mode 100644 index 00000000..1d9b08e0 --- /dev/null +++ b/src/SMAPI/Framework/Serialization/Vector2Converter.cs @@ -0,0 +1,43 @@ +using System; +using Microsoft.Xna.Framework; +using Newtonsoft.Json.Linq; +using StardewModdingAPI.Toolkit.Serialization; +using StardewModdingAPI.Toolkit.Serialization.Converters; + +namespace StardewModdingAPI.Framework.Serialization +{ + /// Handles deserialization of for crossplatform compatibility. + /// + /// - Linux/Mac format: { "X": 1, "Y": 2 } + /// - Windows format: "1, 2" + /// + internal class Vector2Converter : SimpleReadOnlyConverter + { + /********* + ** Protected methods + *********/ + /// Read a JSON object. + /// The JSON object to read. + /// The path to the current JSON node. + protected override Vector2 ReadObject(JObject obj, string path) + { + float x = obj.ValueIgnoreCase(nameof(Vector2.X)); + float y = obj.ValueIgnoreCase(nameof(Vector2.Y)); + return new Vector2(x, y); + } + + /// Read a JSON string. + /// The JSON string value. + /// The path to the current JSON node. + protected override Vector2 ReadString(string str, string path) + { + string[] parts = str.Split(','); + if (parts.Length != 2) + throw new SParseException($"Can't parse {typeof(Vector2).Name} from invalid value '{str}' (path: {path})."); + + float x = Convert.ToSingle(parts[0]); + float y = Convert.ToSingle(parts[1]); + return new Vector2(x, y); + } + } +} -- cgit From d0885831c3698bcb353d76c147500b7ea1dfbc78 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 1 Feb 2020 11:09:21 -0500 Subject: ignore Harmony DLL in mod build package --- docs/technical/mod-package.md | 7 ++++++- src/SMAPI.ModBuildConfig/Framework/ModFileManager.cs | 3 +++ src/SMAPI.ModBuildConfig/package.nuspec | 15 +++------------ src/SMAPI.Mods.ConsoleCommands/manifest.json | 4 ++-- 4 files changed, 14 insertions(+), 15 deletions(-) (limited to 'src') diff --git a/docs/technical/mod-package.md b/docs/technical/mod-package.md index 5b971f96..a54d3011 100644 --- a/docs/technical/mod-package.md +++ b/docs/technical/mod-package.md @@ -287,7 +287,11 @@ That will create a `Pathoschild.Stardew.ModBuildConfig-.nupkg` file in which can be uploaded to NuGet or referenced directly. ## Release notes -### Upcoming release +### 3.1 +* Added support for semantic versioning 2.0. +* `0Harmony.dll` is now ignored if the mod references it directly (it's bundled with SMAPI). + +### 3.0 * Updated for SMAPI 3.0 and Stardew Valley 1.4. * Added automatic support for `assets` folders. * Added `$(GameExecutableName)` MSBuild variable. @@ -298,6 +302,7 @@ which can be uploaded to NuGet or referenced directly. * Fixed `Newtonsoft.Json.pdb` included in release zips when Json.NET is referenced directly. * Fixed `` not working for `i18n` files. * Dropped support for older versions of SMAPI and Visual Studio. +* Migrated package icon to NuGet's new format. ### 2.2 * Added support for SMAPI 2.8+ (still compatible with earlier versions). diff --git a/src/SMAPI.ModBuildConfig/Framework/ModFileManager.cs b/src/SMAPI.ModBuildConfig/Framework/ModFileManager.cs index a852f133..f0363a3e 100644 --- a/src/SMAPI.ModBuildConfig/Framework/ModFileManager.cs +++ b/src/SMAPI.ModBuildConfig/Framework/ModFileManager.cs @@ -156,6 +156,9 @@ namespace StardewModdingAPI.ModBuildConfig.Framework // release zips this.EqualsInvariant(file.Extension, ".zip") + // Harmony (bundled into SMAPI) + || this.EqualsInvariant(file.Name, "0Harmony.dll") + // Json.NET (bundled into SMAPI) || this.EqualsInvariant(file.Name, "Newtonsoft.Json.dll") || this.EqualsInvariant(file.Name, "Newtonsoft.Json.pdb") diff --git a/src/SMAPI.ModBuildConfig/package.nuspec b/src/SMAPI.ModBuildConfig/package.nuspec index 846f438d..c5297b46 100644 --- a/src/SMAPI.ModBuildConfig/package.nuspec +++ b/src/SMAPI.ModBuildConfig/package.nuspec @@ -14,18 +14,9 @@ https://raw.githubusercontent.com/Pathoschild/SMAPI/develop/src/SMAPI.ModBuildConfig/assets/nuget-icon.png Automates the build configuration for crossplatform Stardew Valley SMAPI mods. For SMAPI 3.0 or later. - 3.0.0: - - Updated for SMAPI 3.0 and Stardew Valley 1.4. - - Added automatic support for 'assets' folders. - - Added $(GameExecutableName) MSBuild variable. - - Added support for projects using the simplified .csproj format. - - Added option to disable game debugging config. - - Added .pdb files to builds by default (to enable line numbers in error stack traces). - - Added optional Harmony reference. - - Fixed Newtonsoft.Json.pdb included in release zips when Json.NET is referenced directly. - - Fixed <IgnoreModFilePatterns> not working for i18n files. - - Dropped support for older versions of SMAPI and Visual Studio. - - Migrated package icon to NuGet's new format. + 3.1.0: + - Added support for semantic versioning 2.0. + - 0Harmony.dll is now ignored if the mod references it directly (it's bundled with SMAPI). diff --git a/src/SMAPI.Mods.ConsoleCommands/manifest.json b/src/SMAPI.Mods.ConsoleCommands/manifest.json index 1e12e13e..0d0e4901 100644 --- a/src/SMAPI.Mods.ConsoleCommands/manifest.json +++ b/src/SMAPI.Mods.ConsoleCommands/manifest.json @@ -1,9 +1,9 @@ { "Name": "Console Commands", "Author": "SMAPI", - "Version": "3.1.0", + "Version": "3.2.0", "Description": "Adds SMAPI console commands that let you manipulate the game.", "UniqueID": "SMAPI.ConsoleCommands", "EntryDll": "ConsoleCommands.dll", - "MinimumApiVersion": "3.1.0" + "MinimumApiVersion": "3.2.0" } -- cgit From aeb72586fdd94219cb9ae11cfd9f162765a5bc51 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 1 Feb 2020 11:11:44 -0500 Subject: prepare for release --- build/common.targets | 2 +- docs/release-notes.md | 20 +++++++++++--------- docs/technical/mod-package.md | 2 +- src/SMAPI.ModBuildConfig/package.nuspec | 2 +- src/SMAPI.Mods.SaveBackup/manifest.json | 4 ++-- src/SMAPI/Constants.cs | 2 +- 6 files changed, 17 insertions(+), 15 deletions(-) (limited to 'src') diff --git a/build/common.targets b/build/common.targets index cfdcccca..8b0d1301 100644 --- a/build/common.targets +++ b/build/common.targets @@ -4,7 +4,7 @@ - 3.1.0 + 3.2.0 SMAPI $(AssemblySearchPaths);{GAC} diff --git a/docs/release-notes.md b/docs/release-notes.md index fca18bd2..f1981218 100644 --- a/docs/release-notes.md +++ b/docs/release-notes.md @@ -1,17 +1,18 @@ ← [README](README.md) # Release notes -## Upcoming release +## 3.2 +Released 01 February 2020 for Stardew Valley 1.4.1 or later. * For players: - * SMAPI now prevents crashes due to mods adding invalid schedule data. + * SMAPI now prevents crashes due to invalid schedule data. * SMAPI now prevents crashes due to invalid building types. * Added support for persistent `smapi-internal/config.json` overrides (see info in the file). * Updated minimum game version (1.4 → 1.4.1). * Fixed 'collection was modified' error when returning to title in rare cases. - * Fixed update-check error if a mod's Chucklefish page has no version. - * Fixed SMAPI beta versions not showing update alert on next launch (thanks to danvolchek!). + * Fixed error when update-checking a mod with a Chucklefish page that has no version. * Fixed rare error when building/demolishing buildings. + * Fixed SMAPI beta versions not showing update alert on next launch (thanks to danvolchek!). * For the Console Commands mod: * Added `performance` command to track mod performance metrics. This is an advanced experimental feature. (Thanks to Drachenkätzchen!) @@ -22,20 +23,21 @@ * Fixed Android issue where game files were backed up. * For modders: - * Added support for loading `.tmx` map files. - * Added JSON converter for `Vector2` values, so they work consistently crossplatform. + * Added support for `.tmx` map files. + * Added special handling for `Vector2` values in `.json` files, so they work consistently crossplatform. * Reworked the order that asset editors/loaders are called between multiple mods to support some framework mods like Content Patcher and Json Assets. Note that the order is undefined and should not be depended on. - * Removed invalid-schedule validation which had false positives. + * Fixed incorrect warning about mods adding invalid schedules in some cases. The validation was unreliable, and has been removed. * Fixed asset propagation not updating other players' sprites. * Fixed asset propagation for player sprites not updating recolor maps (e.g. sleeves). - * Fixed `helper.Data.Read/WriteGlobalData` using the `Saves` folder instead of the game's appdata folder. The installer will move existing folders automatically. + * Fixed asset propagation for marriage dialogue. * Fixed dialogue asset changes not correctly propagated until the next day. - * Fixed dialogue asset propagation for marriage dialogue. + * Fixed `helper.Data.Read`/`WriteGlobalData` using the `Saves` folder instead of the game's appdata folder. The installer will move existing folders automatically. * Fixed issue where a mod which implemented `IAssetEditor`/`IAssetLoader` on its entry class could then remove itself from the editor/loader list. * For SMAPI/tool developers: * Added internal performance monitoring (thanks to Drachenkätzchen!). This is disabled by default in the current version, but can be enabled using the `performance` console command. * Added internal support for four-part versions to support SMAPI on Android. + * Rewrote `SemanticVersion` parsing. * Updated links for the new r/SMAPI subreddit. * The `/mods` web API endpoint now includes version mappings from the wiki. * Dropped API support for the pre-3.0 update-check format. diff --git a/docs/technical/mod-package.md b/docs/technical/mod-package.md index a54d3011..e771d7a9 100644 --- a/docs/technical/mod-package.md +++ b/docs/technical/mod-package.md @@ -289,7 +289,7 @@ which can be uploaded to NuGet or referenced directly. ## Release notes ### 3.1 * Added support for semantic versioning 2.0. -* `0Harmony.dll` is now ignored if the mod references it directly (it's bundled with SMAPI). +* `0Harmony.dll` is now ignored if the mod references Harmony directly (it's bundled with SMAPI). ### 3.0 * Updated for SMAPI 3.0 and Stardew Valley 1.4. diff --git a/src/SMAPI.ModBuildConfig/package.nuspec b/src/SMAPI.ModBuildConfig/package.nuspec index c5297b46..afb03cec 100644 --- a/src/SMAPI.ModBuildConfig/package.nuspec +++ b/src/SMAPI.ModBuildConfig/package.nuspec @@ -2,7 +2,7 @@ Pathoschild.Stardew.ModBuildConfig - 3.0.0 + 3.1.0 Build package for SMAPI mods Pathoschild Pathoschild diff --git a/src/SMAPI.Mods.SaveBackup/manifest.json b/src/SMAPI.Mods.SaveBackup/manifest.json index 55af8f35..74256013 100644 --- a/src/SMAPI.Mods.SaveBackup/manifest.json +++ b/src/SMAPI.Mods.SaveBackup/manifest.json @@ -1,9 +1,9 @@ { "Name": "Save Backup", "Author": "SMAPI", - "Version": "3.1.0", + "Version": "3.2.0", "Description": "Automatically backs up all your saves once per day into its folder.", "UniqueID": "SMAPI.SaveBackup", "EntryDll": "SaveBackup.dll", - "MinimumApiVersion": "3.1.0" + "MinimumApiVersion": "3.2.0" } diff --git a/src/SMAPI/Constants.cs b/src/SMAPI/Constants.cs index 8afe4b52..201d3166 100644 --- a/src/SMAPI/Constants.cs +++ b/src/SMAPI/Constants.cs @@ -20,7 +20,7 @@ namespace StardewModdingAPI ** Public ****/ /// SMAPI's current semantic version. - public static ISemanticVersion ApiVersion { get; } = new Toolkit.SemanticVersion("3.1.0"); + public static ISemanticVersion ApiVersion { get; } = new Toolkit.SemanticVersion("3.2.0"); /// The minimum supported version of Stardew Valley. public static ISemanticVersion MinimumGameVersion { get; } = new GameVersion("1.4.1"); -- cgit From 56b1b8d4d2177fbb0673672bc4b7bb49bcccc606 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 1 Feb 2020 15:33:40 -0500 Subject: fix save folder filter in Save Backup --- src/SMAPI.Mods.SaveBackup/ModEntry.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/SMAPI.Mods.SaveBackup/ModEntry.cs b/src/SMAPI.Mods.SaveBackup/ModEntry.cs index b1d368a1..8b139d8f 100644 --- a/src/SMAPI.Mods.SaveBackup/ModEntry.cs +++ b/src/SMAPI.Mods.SaveBackup/ModEntry.cs @@ -71,7 +71,7 @@ namespace StardewModdingAPI.Mods.SaveBackup // copy saves to fallback directory (ignore non-save files/folders) this.Monitor.Log($"Backing up saves to {fallbackDir.FullName}...", LogLevel.Trace); DirectoryInfo savesDir = new DirectoryInfo(Constants.SavesPath); - this.RecursiveCopy(savesDir, fallbackDir, copyRoot: false, entry => this.MatchSaveFolders(savesDir, entry)); + this.RecursiveCopy(savesDir, fallbackDir, entry => this.MatchSaveFolders(savesDir, entry), copyRoot: false); // compress backup if possible this.Monitor.Log("Compressing backup if possible...", LogLevel.Trace); @@ -199,7 +199,7 @@ namespace StardewModdingAPI.Mods.SaveBackup /// Whether to copy the root folder itself, or false to only copy its contents. /// A filter which matches the files or directories to copy, or null to copy everything. /// Derived from the SMAPI installer code. - private void RecursiveCopy(FileSystemInfo source, DirectoryInfo targetFolder, bool copyRoot = true, Func filter = null) + private void RecursiveCopy(FileSystemInfo source, DirectoryInfo targetFolder, Func filter, bool copyRoot = true) { if (!targetFolder.Exists) targetFolder.Create(); @@ -216,7 +216,7 @@ namespace StardewModdingAPI.Mods.SaveBackup case DirectoryInfo sourceDir: DirectoryInfo targetSubfolder = copyRoot ? new DirectoryInfo(Path.Combine(targetFolder.FullName, sourceDir.Name)) : targetFolder; foreach (var entry in sourceDir.EnumerateFileSystemInfos()) - this.RecursiveCopy(entry, targetSubfolder); + this.RecursiveCopy(entry, targetSubfolder, filter); break; default: -- cgit From 17a9193fd28c527dcba40360702adb277736cc45 Mon Sep 17 00:00:00 2001 From: Jesse Plamondon-Willard Date: Sat, 1 Feb 2020 16:06:12 -0500 Subject: update packages --- .../SMAPI.ModBuildConfig.Analyzer.Tests.csproj | 5 ++++- src/SMAPI.Toolkit/SMAPI.Toolkit.csproj | 2 +- .../SMAPI.Web.LegacyRedirects.csproj | 2 +- src/SMAPI.Web/SMAPI.Web.csproj | 14 +++++++------- src/SMAPI/SMAPI.csproj | 2 +- 5 files changed, 14 insertions(+), 11 deletions(-) (limited to 'src') diff --git a/src/SMAPI.ModBuildConfig.Analyzer.Tests/SMAPI.ModBuildConfig.Analyzer.Tests.csproj b/src/SMAPI.ModBuildConfig.Analyzer.Tests/SMAPI.ModBuildConfig.Analyzer.Tests.csproj index 7e3ce7d4..c1d5626f 100644 --- a/src/SMAPI.ModBuildConfig.Analyzer.Tests/SMAPI.ModBuildConfig.Analyzer.Tests.csproj +++ b/src/SMAPI.ModBuildConfig.Analyzer.Tests/SMAPI.ModBuildConfig.Analyzer.Tests.csproj @@ -9,7 +9,10 @@ - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/src/SMAPI.Toolkit/SMAPI.Toolkit.csproj b/src/SMAPI.Toolkit/SMAPI.Toolkit.csproj index eaf8510b..16a97dbf 100644 --- a/src/SMAPI.Toolkit/SMAPI.Toolkit.csproj +++ b/src/SMAPI.Toolkit/SMAPI.Toolkit.csproj @@ -12,7 +12,7 @@ - + diff --git a/src/SMAPI.Web.LegacyRedirects/SMAPI.Web.LegacyRedirects.csproj b/src/SMAPI.Web.LegacyRedirects/SMAPI.Web.LegacyRedirects.csproj index a3d5c2b6..36831961 100644 --- a/src/SMAPI.Web.LegacyRedirects/SMAPI.Web.LegacyRedirects.csproj +++ b/src/SMAPI.Web.LegacyRedirects/SMAPI.Web.LegacyRedirects.csproj @@ -9,7 +9,7 @@ - + diff --git a/src/SMAPI.Web/SMAPI.Web.csproj b/src/SMAPI.Web/SMAPI.Web.csproj index 504254cd..148631a9 100644 --- a/src/SMAPI.Web/SMAPI.Web.csproj +++ b/src/SMAPI.Web/SMAPI.Web.csproj @@ -12,21 +12,21 @@ - - + + - - + + - + - - + + diff --git a/src/SMAPI/SMAPI.csproj b/src/SMAPI/SMAPI.csproj index 579af423..c5d0f247 100644 --- a/src/SMAPI/SMAPI.csproj +++ b/src/SMAPI/SMAPI.csproj @@ -19,7 +19,7 @@ - + -- cgit