diff options
Diffstat (limited to 'src/SMAPI')
-rw-r--r-- | src/SMAPI/Constants.cs | 2 | ||||
-rw-r--r-- | src/SMAPI/Framework/Events/EventManager.cs | 5 | ||||
-rw-r--r-- | src/SMAPI/Framework/Events/IManagedEvent.cs | 7 | ||||
-rw-r--r-- | src/SMAPI/Framework/Events/ManagedEvent.cs | 103 | ||||
-rw-r--r-- | src/SMAPI/Framework/PerformanceCounter/AlertContext.cs | 14 | ||||
-rw-r--r-- | src/SMAPI/Framework/PerformanceCounter/AlertEntry.cs | 20 | ||||
-rw-r--r-- | src/SMAPI/Framework/PerformanceCounter/EventPerformanceCounterCategory.cs | 16 | ||||
-rw-r--r-- | src/SMAPI/Framework/PerformanceCounter/EventPerformanceCounterCollection.cs | 11 | ||||
-rw-r--r-- | src/SMAPI/Framework/PerformanceCounter/IPerformanceCounterEvent.cs | 1 | ||||
-rw-r--r-- | src/SMAPI/Framework/PerformanceCounter/PerformanceCounter.cs | 18 | ||||
-rw-r--r-- | src/SMAPI/Framework/PerformanceCounter/PerformanceCounterCollection.cs | 144 | ||||
-rw-r--r-- | src/SMAPI/Framework/PerformanceCounter/PerformanceCounterManager.cs | 233 | ||||
-rw-r--r-- | src/SMAPI/Framework/SCore.cs | 193 | ||||
-rw-r--r-- | src/SMAPI/Framework/SGame.cs | 10 | ||||
-rw-r--r-- | src/SMAPI/Program.cs | 2 |
15 files changed, 429 insertions, 350 deletions
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"; /// <summary>The URL of the SMAPI home page.</summary> - internal const string GamePerformanceCounterName = "-internal-"; + internal const string GamePerformanceCounterName = "<StardewValley>"; /// <summary>The absolute path to the folder containing SMAPI's internal files.</summary> 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 /// <summary>Construct an instance.</summary> /// <param name="monitor">Writes messages to the log.</param> /// <param name="modRegistry">The mod registry with which to identify mods.</param> - public EventManager(IMonitor monitor, ModRegistry modRegistry) + public EventManager(IMonitor monitor, ModRegistry modRegistry, PerformanceCounterManager performanceCounterManager) { // create shortcut initializers - ManagedEvent<TEventArgs> ManageEventOf<TEventArgs>(string typeName, string eventName) => new ManagedEvent<TEventArgs>($"{typeName}.{eventName}", monitor, modRegistry); + ManagedEvent<TEventArgs> ManageEventOf<TEventArgs>(string typeName, string eventName) => new ManagedEvent<TEventArgs>($"{typeName}.{eventName}", monitor, modRegistry, performanceCounterManager); // init events (new) this.MenuChanged = ManageEventOf<MenuChangedEventArgs>(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 { /// <summary>An event wrapper which intercepts and logs errors in handler code.</summary> /// <typeparam name="TEventArgs">The event arguments type.</typeparam> - internal class ManagedEvent<TEventArgs>: IPerformanceCounterEvent + internal class ManagedEvent<TEventArgs>: IManagedEvent { /********* ** Fields @@ -32,38 +30,8 @@ namespace StardewModdingAPI.Framework.Events /// <summary>The cached invocation list.</summary> private EventHandler<TEventArgs>[] CachedInvocationList; - public IDictionary<string, PerformanceCounter.PerformanceCounter> PerformanceCounters { get; } = new Dictionary<string, PerformanceCounter.PerformanceCounter>(); - - 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()); - } + /// <summary>The performance counter manager.</summary> + private readonly PerformanceCounterManager PerformanceCounterManager; /********* ** Public methods @@ -72,11 +40,18 @@ namespace StardewModdingAPI.Framework.Events /// <param name="eventName">A human-readable name for the event.</param> /// <param name="monitor">Writes messages to the log.</param> /// <param name="modRegistry">The mod registry with which to identify mods.</param> - 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; + } + + /// <summary>Gets the event name.</summary> + public string GetName() + { + return this.EventName; } /// <summary>Get whether anything is listening to the event.</summary> @@ -99,8 +74,6 @@ namespace StardewModdingAPI.Framework.Events { this.Event += handler; this.AddTracking(mod, handler, this.Event?.GetInvocationList().Cast<EventHandler<TEventArgs>>()); - - } /// <summary>Remove an event handler.</summary> @@ -111,18 +84,6 @@ namespace StardewModdingAPI.Framework.Events this.RemoveTracking(handler, this.Event?.GetInvocationList().Cast<EventHandler<TEventArgs>>()); } - public long GetAverageCallsPerSecond() - { - long runtimeInSeconds = (long)DateTime.Now.Subtract(this.StartDateTime).TotalSeconds; - - if (runtimeInSeconds == 0) - { - return 0; - } - - return this.EventCallCount / runtimeInSeconds; - } - /// <summary>Raise the event and notify all handlers.</summary> /// <param name="args">The event arguments to pass.</param> 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<TEventArgs> 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); } /// <summary>Raise the event and notify all handlers.</summary> @@ -191,6 +138,20 @@ namespace StardewModdingAPI.Framework.Events /********* ** Private methods *********/ + + private string GetModNameForPerformanceCounters(EventHandler<TEventArgs> handler) + { + IModMetadata mod = this.GetSourceMod(handler); + + if (mod == null) + { + return Constants.GamePerformanceCounterName; + } + + return mod.HasManifest() ? mod.Manifest.UniqueID : mod.DisplayName; + + } + /// <summary>Track an event handler.</summary> /// <param name="mod">The mod which added the handler.</param> /// <param name="handler">The event handler.</param> 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<AlertContext> Context; + + public AlertEntry(PerformanceCounterCollection collection, double executionTimeMilliseconds, double threshold, List<AlertContext> 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<string, PerformanceCounter.PerformanceCounter> 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<PerformanceCounterEntry> _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<PerformanceCounterEntry>(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<string, PerformanceCounter> PerformanceCounters { get; } = new Dictionary<string, PerformanceCounter>(); + 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<AlertContext> TriggeredPerformanceCounters = new List<AlertContext>(); + + 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<AlertContext> 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<AlertContext>() {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<EventPerformanceCounterCategory> PerformanceCounterEvents = new HashSet<EventPerformanceCounterCategory>(); + public HashSet<PerformanceCounterCollection> PerformanceCounterCollections = new HashSet<PerformanceCounterCollection>(); + public List<AlertEntry> Alerts = new List<AlertEntry>(); + 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) + /// <summary>Print any queued messages.</summary> + 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<EventPerformanceCounterCategory>() + 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<PerformanceCounterCollection>() + { + 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 /// <summary>Manages SMAPI events for mods.</summary> private readonly EventManager EventManager; - private readonly PerformanceCounterManager PerformanceCounterManager; - /// <summary>Whether the game is currently running.</summary> private bool IsGameRunning; @@ -137,6 +134,10 @@ namespace StardewModdingAPI.Framework /// <remarks>This is initialized after the game starts. This is accessed directly because it's not part of the normal class model.</remarks> internal static DeprecationManager DeprecationManager { get; private set; } + /// <summary>Manages performance counters.</summary> + /// <remarks>This is initialized after the game starts. This is accessed directly because it's not part of the normal class model.</remarks> + internal static PerformanceCounterManager PerformanceCounterManager { get; private set; } + /********* ** 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 <cmd>' 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 <cmd>\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}'."); } } - /// <summary>Get an ASCII table to show tabular data in the console.</summary> - /// <typeparam name="T">The data type.</typeparam> - /// <param name="data">The data to display.</param> - /// <param name="header">The table header.</param> - /// <param name="getRow">Returns a set of fields for a data value.</param> - protected string GetTableString<T>(IEnumerable<T> data, string[] header, Func<T, string[]> getRow) - { - // 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<string[]> lines = new List<string[]>(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<string> 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<EventPerformanceCounterCategory> 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); - } - /// <summary>Redirect messages logged directly to the console to the given monitor.</summary> /// <param name="gameMonitor">The monitor with which to log messages as the game.</param> /// <param name="message">The message to log.</param> 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 /// <summary>Manages deprecation warnings.</summary> private readonly DeprecationManager DeprecationManager; + private readonly PerformanceCounterManager PerformanceCounterManager; + /// <summary>The maximum number of consecutive attempts SMAPI should make to recover from a draw error.</summary> private readonly Countdown DrawCrashTimer = new Countdown(60); // 60 ticks = roughly one second @@ -152,11 +155,12 @@ namespace StardewModdingAPI.Framework /// <param name="jsonHelper">Encapsulates SMAPI's JSON file parsing.</param> /// <param name="modRegistry">Tracks the installed mods.</param> /// <param name="deprecationManager">Manages deprecation warnings.</param> + /// <param name="performanceCounterManager">Manages performance monitoring.</param> /// <param name="onGameInitialized">A callback to invoke after the game finishes initializing.</param> /// <param name="onGameExiting">A callback to invoke when the game exits.</param> /// <param name="cancellationToken">Propagates notification that SMAPI should exit.</param> /// <param name="logNetworkTraffic">Whether to log network traffic.</param> - internal SGame(Monitor monitor, IMonitor monitorForGame, Reflector reflection, Translator translator, EventManager eventManager, JsonHelper jsonHelper, ModRegistry modRegistry, DeprecationManager deprecationManager, 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 { |