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(-) 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 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 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(-) 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 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 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 EventPerfor