diff options
Diffstat (limited to 'src/SMAPI/Framework/PerformanceCounter')
8 files changed, 291 insertions, 372 deletions
diff --git a/src/SMAPI/Framework/PerformanceCounter/AlertContext.cs b/src/SMAPI/Framework/PerformanceCounter/AlertContext.cs index 63f0a5ed..76c472e1 100644 --- a/src/SMAPI/Framework/PerformanceCounter/AlertContext.cs +++ b/src/SMAPI/Framework/PerformanceCounter/AlertContext.cs @@ -3,13 +3,20 @@ namespace StardewModdingAPI.Framework.PerformanceCounter /// <summary>The context for an alert.</summary> internal struct AlertContext { + /********* + ** Accessors + *********/ /// <summary>The source which triggered the alert.</summary> - public readonly string Source; + public string Source { get; } /// <summary>The elapsed milliseconds.</summary> - public readonly double Elapsed; + public double Elapsed { get; } - /// <summary>Creates a new alert context.</summary> + + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> /// <param name="source">The source which triggered the alert.</param> /// <param name="elapsed">The elapsed milliseconds.</param> public AlertContext(string source, double elapsed) @@ -18,6 +25,7 @@ namespace StardewModdingAPI.Framework.PerformanceCounter this.Elapsed = elapsed; } + /// <summary>Get a human-readable text form of this instance.</summary> public override string ToString() { return $"{this.Source}: {this.Elapsed:F2}ms"; diff --git a/src/SMAPI/Framework/PerformanceCounter/AlertEntry.cs b/src/SMAPI/Framework/PerformanceCounter/AlertEntry.cs index b87d8642..494f34a9 100644 --- a/src/SMAPI/Framework/PerformanceCounter/AlertEntry.cs +++ b/src/SMAPI/Framework/PerformanceCounter/AlertEntry.cs @@ -1,28 +1,33 @@ -using System.Collections.Generic; - namespace StardewModdingAPI.Framework.PerformanceCounter { /// <summary>A single alert entry.</summary> internal struct AlertEntry { + /********* + ** Accessors + *********/ /// <summary>The collection in which the alert occurred.</summary> - public readonly PerformanceCounterCollection Collection; + public PerformanceCounterCollection Collection { get; } /// <summary>The actual execution time in milliseconds.</summary> - public readonly double ExecutionTimeMilliseconds; + public double ExecutionTimeMilliseconds { get; } + + /// <summary>The configured alert threshold in milliseconds.</summary> + public double ThresholdMilliseconds { get; } - /// <summary>The configured alert threshold. </summary> - public readonly double ThresholdMilliseconds; + /// <summary>The sources involved in exceeding the threshold.</summary> + public AlertContext[] Context { get; } - /// <summary>The context list, which records all sources involved in exceeding the threshold.</summary> - public readonly List<AlertContext> Context; - /// <summary>Creates a new alert entry.</summary> - /// <param name="collection">The source collection in which the alert occurred.</param> + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + /// <param name="collection">The collection in which the alert occurred.</param> /// <param name="executionTimeMilliseconds">The actual execution time in milliseconds.</param> - /// <param name="thresholdMilliseconds">The configured threshold in milliseconds.</param> - /// <param name="context">A list of AlertContext to record which sources were involved</param> - public AlertEntry(PerformanceCounterCollection collection, double executionTimeMilliseconds, double thresholdMilliseconds, List<AlertContext> context) + /// <param name="thresholdMilliseconds">The configured alert threshold in milliseconds.</param> + /// <param name="context">The sources involved in exceeding the threshold.</param> + public AlertEntry(PerformanceCounterCollection collection, double executionTimeMilliseconds, double thresholdMilliseconds, AlertContext[] context) { this.Collection = collection; this.ExecutionTimeMilliseconds = executionTimeMilliseconds; diff --git a/src/SMAPI/Framework/PerformanceCounter/EventPerformanceCounterCollection.cs b/src/SMAPI/Framework/PerformanceCounter/EventPerformanceCounterCollection.cs deleted file mode 100644 index 4690c512..00000000 --- a/src/SMAPI/Framework/PerformanceCounter/EventPerformanceCounterCollection.cs +++ /dev/null @@ -1,16 +0,0 @@ -using StardewModdingAPI.Framework.Events; - -namespace StardewModdingAPI.Framework.PerformanceCounter -{ - /// <summary>Represents a performance counter collection specific to game events.</summary> - internal class EventPerformanceCounterCollection: PerformanceCounterCollection - { - /// <summary>Creates a new event performance counter collection.</summary> - /// <param name="manager">The performance counter manager.</param> - /// <param name="event">The ManagedEvent.</param> - /// <param name="isImportant">If the event is flagged as important.</param> - public EventPerformanceCounterCollection(PerformanceCounterManager manager, IManagedEvent @event, bool isImportant) : base(manager, @event.GetName(), isImportant) - { - } - } -} diff --git a/src/SMAPI/Framework/PerformanceCounter/PeakEntry.cs b/src/SMAPI/Framework/PerformanceCounter/PeakEntry.cs index 95dc11f4..abb29541 100644 --- a/src/SMAPI/Framework/PerformanceCounter/PeakEntry.cs +++ b/src/SMAPI/Framework/PerformanceCounter/PeakEntry.cs @@ -1,20 +1,31 @@ using System; -using System.Collections.Generic; namespace StardewModdingAPI.Framework.PerformanceCounter { + /// <summary>A peak invocation time.</summary> internal struct PeakEntry { + /********* + ** Accessors + *********/ /// <summary>The actual execution time in milliseconds.</summary> - public readonly double ExecutionTimeMilliseconds; + public double ExecutionTimeMilliseconds { get; } - /// <summary>The DateTime when the entry occured.</summary> - public DateTime EventTime; + /// <summary>When the entry occurred.</summary> + public DateTime EventTime { get; } - /// <summary>The context list, which records all sources involved in exceeding the threshold.</summary> - public readonly List<AlertContext> Context; + /// <summary>The sources involved in exceeding the threshold.</summary> + public AlertContext[] Context { get; } - public PeakEntry(double executionTimeMilliseconds, DateTime eventTime, List<AlertContext> context) + + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + /// <param name="executionTimeMilliseconds">The actual execution time in milliseconds.</param> + /// <param name="eventTime">When the entry occurred.</param> + /// <param name="context">The sources involved in exceeding the threshold.</param> + public PeakEntry(double executionTimeMilliseconds, DateTime eventTime, AlertContext[] context) { this.ExecutionTimeMilliseconds = executionTimeMilliseconds; this.EventTime = eventTime; diff --git a/src/SMAPI/Framework/PerformanceCounter/PerformanceCounter.cs b/src/SMAPI/Framework/PerformanceCounter/PerformanceCounter.cs index e9dfcb14..0fb6482d 100644 --- a/src/SMAPI/Framework/PerformanceCounter/PerformanceCounter.cs +++ b/src/SMAPI/Framework/PerformanceCounter/PerformanceCounter.cs @@ -1,23 +1,32 @@ using System; +using System.Collections.Generic; using System.Linq; -using Cyotek.Collections.Generic; +using Harmony; namespace StardewModdingAPI.Framework.PerformanceCounter { + /// <summary>Tracks metadata about a particular code event.</summary> internal class PerformanceCounter { + /********* + ** Fields + *********/ /// <summary>The size of the ring buffer.</summary> - private const int MAX_ENTRIES = 16384; + private readonly int MaxEntries = 16384; /// <summary>The collection to which this performance counter belongs.</summary> private readonly PerformanceCounterCollection ParentCollection; - /// <summary>The circular buffer which stores all performance counter entries</summary> - private readonly CircularBuffer<PerformanceCounterEntry> _counter; + /// <summary>The performance counter entries.</summary> + private readonly Stack<PerformanceCounterEntry> Entries; - /// <summary>The peak execution time</summary> + /// <summary>The entry with the highest execution time.</summary> private PerformanceCounterEntry? PeakPerformanceCounterEntry; + + /********* + ** Accessors + *********/ /// <summary>The name of the source.</summary> public string Source { get; } @@ -27,118 +36,90 @@ namespace StardewModdingAPI.Framework.PerformanceCounter /// <summary>If alerting is enabled or not</summary> public bool EnableAlerts { get; set; } + + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + /// <param name="parentCollection">The collection to which this performance counter belongs.</param> + /// <param name="source">The name of the source.</param> public PerformanceCounter(PerformanceCounterCollection parentCollection, string source) { this.ParentCollection = parentCollection; this.Source = source; - this._counter = new CircularBuffer<PerformanceCounterEntry>(PerformanceCounter.MAX_ENTRIES); + this.Entries = new Stack<PerformanceCounterEntry>(this.MaxEntries); } - /// <summary>Adds a new performance counter entry to the list. Updates the peak entry and adds an alert if - /// monitoring is enabled and the execution time exceeds the threshold.</summary> + /// <summary>Add a performance counter entry to the list, update monitoring, and raise alerts if needed.</summary> /// <param name="entry">The entry to add.</param> public void Add(PerformanceCounterEntry entry) { - this._counter.Put(entry); - - if (this.EnableAlerts && entry.ElapsedMilliseconds > this.AlertThresholdMilliseconds) - this.ParentCollection.AddAlert(entry.ElapsedMilliseconds, this.AlertThresholdMilliseconds, - new AlertContext(this.Source, entry.ElapsedMilliseconds)); + // add entry + if (this.Entries.Count > this.MaxEntries) + this.Entries.Pop(); + this.Entries.Add(entry); - if (this.PeakPerformanceCounterEntry == null) + // update metrics + if (this.PeakPerformanceCounterEntry == null || entry.ElapsedMilliseconds > this.PeakPerformanceCounterEntry.Value.ElapsedMilliseconds) this.PeakPerformanceCounterEntry = entry; - else - { - if (entry.ElapsedMilliseconds > this.PeakPerformanceCounterEntry.Value.ElapsedMilliseconds) - this.PeakPerformanceCounterEntry = entry; - } + + // raise alert + if (this.EnableAlerts && entry.ElapsedMilliseconds > this.AlertThresholdMilliseconds) + this.ParentCollection.AddAlert(entry.ElapsedMilliseconds, this.AlertThresholdMilliseconds, new AlertContext(this.Source, entry.ElapsedMilliseconds)); } - /// <summary>Clears all performance counter entries and resets the peak entry.</summary> + /// <summary>Clear all performance counter entries and monitoring.</summary> public void Reset() { - this._counter.Clear(); + this.Entries.Clear(); this.PeakPerformanceCounterEntry = null; } - /// <summary>Returns the peak entry.</summary> - /// <returns>The peak entry.</returns> + /// <summary>Get the peak entry.</summary> public PerformanceCounterEntry? GetPeak() { return this.PeakPerformanceCounterEntry; } - /// <summary>Returns the peak entry.</summary> - /// <returns>The peak entry.</returns> - public PerformanceCounterEntry? GetPeak(TimeSpan range, DateTime? relativeTo = null) + /// <summary>Get the entry with the highest execution time.</summary> + /// <param name="range">The time range to search.</param> + /// <param name="endTime">The end time for the <paramref name="range"/>, or null for the current time.</param> + public PerformanceCounterEntry? GetPeak(TimeSpan range, DateTime? endTime = null) { - if (this._counter.IsEmpty) - return null; - - if (relativeTo == null) - relativeTo = DateTime.UtcNow; - - DateTime start = relativeTo.Value.Subtract(range); - - var entries = this._counter.Where(x => (x.EventTime >= start) && (x.EventTime <= relativeTo)).ToList(); - - if (!entries.Any()) - return null; + endTime ??= DateTime.UtcNow; + DateTime startTime = endTime.Value.Subtract(range); - return entries.OrderByDescending(x => x.ElapsedMilliseconds).First(); + return this.Entries + .Where(entry => entry.EventTime >= startTime && entry.EventTime <= endTime) + .OrderByDescending(x => x.ElapsedMilliseconds) + .FirstOrDefault(); } - /// <summary>Resets the peak entry.</summary> - public void ResetPeak() - { - this.PeakPerformanceCounterEntry = null; - } - - /// <summary>Returns the last entry added to the list.</summary> - /// <returns>The last entry</returns> + /// <summary>Get the last entry added to the list.</summary> public PerformanceCounterEntry? GetLastEntry() { - if (this._counter.IsEmpty) + if (this.Entries.Count == 0) return null; - return this._counter.PeekLast(); + return this.Entries.Peek(); } - /// <summary>Returns the average execution time of all entries.</summary> - /// <returns>The average execution time in milliseconds.</returns> - public double GetAverage() + /// <summary>Get the average over a given time span.</summary> + /// <param name="range">The time range to search.</param> + /// <param name="endTime">The end time for the <paramref name="range"/>, or null for the current time.</param> + public double GetAverage(TimeSpan range, DateTime? endTime = null) { - if (this._counter.IsEmpty) - return 0; - - return this._counter.Average(p => p.ElapsedMilliseconds); - } - - /// <summary>Returns the average over a given time span.</summary> - /// <param name="range">The time range to retrieve.</param> - /// <param name="relativeTo">The DateTime from which to start the average. Defaults to DateTime.UtcNow if null</param> - /// <returns>The average execution time in milliseconds.</returns> - /// <remarks> - /// The relativeTo parameter specifies from which point in time the range is subtracted. Example: - /// If DateTime is set to 60 seconds ago, and the range is set to 60 seconds, the method would return - /// the average between all entries between 120s ago and 60s ago. - /// </remarks> - public double GetAverage(TimeSpan range, DateTime? relativeTo = null) - { - if (this._counter.IsEmpty) - return 0; - - if (relativeTo == null) - relativeTo = DateTime.UtcNow; - - DateTime start = relativeTo.Value.Subtract(range); - - var entries = this._counter.Where(x => (x.EventTime >= start) && (x.EventTime <= relativeTo)).ToList(); + endTime ??= DateTime.UtcNow; + DateTime startTime = endTime.Value.Subtract(range); - if (!entries.Any()) - return 0; + double[] entries = this.Entries + .Where(entry => entry.EventTime >= startTime && entry.EventTime <= endTime) + .Select(p => p.ElapsedMilliseconds) + .ToArray(); - return entries.Average(x => x.ElapsedMilliseconds); + return entries.Length > 0 + ? entries.Average() + : 0; } } } diff --git a/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterCollection.cs b/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterCollection.cs index f469eceb..bd13a36e 100644 --- a/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterCollection.cs +++ b/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterCollection.cs @@ -2,153 +2,129 @@ using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; -using Cyotek.Collections.Generic; namespace StardewModdingAPI.Framework.PerformanceCounter { internal class PerformanceCounterCollection { - /// <summary>The size of the ring buffer.</summary> - private const int MAX_ENTRIES = 16384; + /********* + ** Fields + *********/ + /// <summary>The number of peak invocations to keep.</summary> + private readonly int MaxEntries = 16384; - /// <summary>The list of triggered performance counters.</summary> + /// <summary>The sources involved in exceeding alert thresholds.</summary> private readonly List<AlertContext> TriggeredPerformanceCounters = new List<AlertContext>(); /// <summary>The stopwatch used to track the invocation time.</summary> private readonly Stopwatch InvocationStopwatch = new Stopwatch(); /// <summary>The performance counter manager.</summary> - private readonly PerformanceCounterManager PerformanceCounterManager; + private readonly PerformanceMonitor PerformanceMonitor; - /// <summary>Holds the time to calculate the average calls per second.</summary> + /// <summary>The time to calculate average calls per second.</summary> private DateTime CallsPerSecondStart = DateTime.UtcNow; - /// <summary>The number of invocations of this collection.</summary> + /// <summary>The number of invocations.</summary> private long CallCount; - /// <summary>The circular buffer which stores all peak invocations</summary> - private readonly CircularBuffer<PeakEntry> PeakInvocations; + /// <summary>The peak invocations.</summary> + private readonly Stack<PeakEntry> PeakInvocations; + + /********* + ** Accessors + *********/ /// <summary>The associated performance counters.</summary> public IDictionary<string, PerformanceCounter> PerformanceCounters { get; } = new Dictionary<string, PerformanceCounter>(); /// <summary>The name of this collection.</summary> public string Name { get; } - /// <summary>Flag if this collection is important (used for the console summary command).</summary> - public bool IsImportant { get; } + /// <summary>Whether the source is typically invoked at least once per second.</summary> + public bool IsPerformanceCritical { get; } /// <summary>The alert threshold in milliseconds.</summary> public double AlertThresholdMilliseconds { get; set; } - /// <summary>If alerting is enabled or not</summary> + /// <summary>Whether alerts are enabled.</summary> public bool EnableAlerts { get; set; } - public PerformanceCounterCollection(PerformanceCounterManager performanceCounterManager, string name, bool isImportant) + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + /// <param name="performanceMonitor">The performance counter manager.</param> + /// <param name="name">The name of this collection.</param> + /// <param name="isPerformanceCritical">Whether the source is typically invoked at least once per second.</param> + public PerformanceCounterCollection(PerformanceMonitor performanceMonitor, string name, bool isPerformanceCritical = false) { - this.PeakInvocations = new CircularBuffer<PeakEntry>(PerformanceCounterCollection.MAX_ENTRIES); + this.PeakInvocations = new Stack<PeakEntry>(this.MaxEntries); this.Name = name; - this.PerformanceCounterManager = performanceCounterManager; - this.IsImportant = isImportant; + this.PerformanceMonitor = performanceMonitor; + this.IsPerformanceCritical = isPerformanceCritical; } - public PerformanceCounterCollection(PerformanceCounterManager performanceCounterManager, string name) - { - this.PeakInvocations = new CircularBuffer<PeakEntry>(PerformanceCounterCollection.MAX_ENTRIES); - this.PerformanceCounterManager = performanceCounterManager; - this.Name = name; - } - - /// <summary>Tracks a single invocation for a named source.</summary> + /// <summary>Track a single invocation for a named source.</summary> /// <param name="source">The name of the source.</param> /// <param name="entry">The entry.</param> public void Track(string source, PerformanceCounterEntry entry) { + // add entry if (!this.PerformanceCounters.ContainsKey(source)) this.PerformanceCounters.Add(source, new PerformanceCounter(this, source)); - this.PerformanceCounters[source].Add(entry); + // raise alert if (this.EnableAlerts) this.TriggeredPerformanceCounters.Add(new AlertContext(source, entry.ElapsedMilliseconds)); } - /// <summary>Returns the average execution time for all non-game internal sources.</summary> - /// <returns>The average execution time in milliseconds</returns> - public double GetModsAverageExecutionTime() - { - return this.PerformanceCounters.Where(p => - p.Key != Constants.GamePerformanceCounterName).Sum(p => p.Value.GetAverage()); - } - - /// <summary>Returns the average execution time for all non-game internal sources.</summary> + /// <summary>Get the average execution time for all non-game internal sources in milliseconds.</summary> /// <param name="interval">The interval for which to get the average, relative to now</param> - /// <returns>The average execution time in milliseconds</returns> public double GetModsAverageExecutionTime(TimeSpan interval) { - return this.PerformanceCounters.Where(p => - p.Key != Constants.GamePerformanceCounterName).Sum(p => p.Value.GetAverage(interval)); - } - - /// <summary>Returns the overall average execution time.</summary> - /// <returns>The average execution time in milliseconds</returns> - public double GetAverageExecutionTime() - { - return this.PerformanceCounters.Sum(p => p.Value.GetAverage()); + return this.PerformanceCounters + .Where(entry => entry.Key != Constants.GamePerformanceCounterName) + .Sum(entry => entry.Value.GetAverage(interval)); } - /// <summary>Returns the overall average execution time.</summary> + /// <summary>Get the overall average execution time in milliseconds.</summary> /// <param name="interval">The interval for which to get the average, relative to now</param> - /// <returns>The average execution time in milliseconds</returns> public double GetAverageExecutionTime(TimeSpan interval) { - return this.PerformanceCounters.Sum(p => p.Value.GetAverage(interval)); + return this.PerformanceCounters + .Sum(entry => entry.Value.GetAverage(interval)); } - /// <summary>Returns the average execution time for game-internal sources.</summary> - /// <returns>The average execution time in milliseconds</returns> - public double GetGameAverageExecutionTime() - { - if (this.PerformanceCounters.TryGetValue(Constants.GamePerformanceCounterName, out PerformanceCounter gameExecTime)) - return gameExecTime.GetAverage(); - - return 0; - } - - /// <summary>Returns the average execution time for game-internal sources.</summary> - /// <returns>The average execution time in milliseconds</returns> + /// <summary>Get the average execution time for game-internal sources in milliseconds.</summary> public double GetGameAverageExecutionTime(TimeSpan interval) { - if (this.PerformanceCounters.TryGetValue(Constants.GamePerformanceCounterName, out PerformanceCounter gameExecTime)) - return gameExecTime.GetAverage(interval); - - return 0; + return this.PerformanceCounters.TryGetValue(Constants.GamePerformanceCounterName, out PerformanceCounter gameExecTime) + ? gameExecTime.GetAverage(interval) + : 0; } - /// <summary>Returns the peak execution time</summary> - /// <param name="interval">The interval for which to get the peak, relative to <paramref name="relativeTo"/></param> - /// <param name="relativeTo">The DateTime which the <paramref name="interval"/> is relative to, or DateTime.Now if not given</param> - /// <returns>The peak execution time</returns> - public double GetPeakExecutionTime(TimeSpan range, DateTime? relativeTo = null) + /// <summary>Get the peak execution time in milliseconds.</summary> + /// <param name="range">The time range to search.</param> + /// <param name="endTime">The end time for the <paramref name="range"/>, or null for the current time.</param> + public double GetPeakExecutionTime(TimeSpan range, DateTime? endTime = null) { - if (this.PeakInvocations.IsEmpty) + if (this.PeakInvocations.Count == 0) return 0; - if (relativeTo == null) - relativeTo = DateTime.UtcNow; - - DateTime start = relativeTo.Value.Subtract(range); - - var entries = this.PeakInvocations.Where(x => (x.EventTime >= start) && (x.EventTime <= relativeTo)).ToList(); + endTime ??= DateTime.UtcNow; + DateTime startTime = endTime.Value.Subtract(range); - if (!entries.Any()) - return 0; - - return entries.OrderByDescending(x => x.ExecutionTimeMilliseconds).First().ExecutionTimeMilliseconds; + return this.PeakInvocations + .Where(entry => entry.EventTime >= startTime && entry.EventTime <= endTime) + .OrderByDescending(x => x.ExecutionTimeMilliseconds) + .Select(p => p.ExecutionTimeMilliseconds) + .FirstOrDefault(); } - /// <summary>Begins tracking the invocation of this collection.</summary> + /// <summary>Start tracking the invocation of this collection.</summary> public void BeginTrackInvocation() { this.TriggeredPerformanceCounters.Clear(); @@ -158,60 +134,58 @@ namespace StardewModdingAPI.Framework.PerformanceCounter this.CallCount++; } - /// <summary>Ends tracking the invocation of this collection. Also records an alert if alerting is enabled - /// and the invocation time exceeds the threshold.</summary> + /// <summary>End tracking the invocation of this collection, and raise an alert if needed.</summary> public void EndTrackInvocation() { this.InvocationStopwatch.Stop(); - this.PeakInvocations.Put( - new PeakEntry(this.InvocationStopwatch.Elapsed.TotalMilliseconds, - DateTime.UtcNow, - this.TriggeredPerformanceCounters)); - - if (!this.EnableAlerts) return; + // add invocation + if (this.PeakInvocations.Count >= this.MaxEntries) + this.PeakInvocations.Pop(); + this.PeakInvocations.Push(new PeakEntry(this.InvocationStopwatch.Elapsed.TotalMilliseconds, DateTime.UtcNow, this.TriggeredPerformanceCounters.ToArray())); - if (this.InvocationStopwatch.Elapsed.TotalMilliseconds >= this.AlertThresholdMilliseconds) - this.AddAlert(this.InvocationStopwatch.Elapsed.TotalMilliseconds, - this.AlertThresholdMilliseconds, this.TriggeredPerformanceCounters); + // raise alert + if (this.EnableAlerts && this.InvocationStopwatch.Elapsed.TotalMilliseconds >= this.AlertThresholdMilliseconds) + this.AddAlert(this.InvocationStopwatch.Elapsed.TotalMilliseconds, this.AlertThresholdMilliseconds, this.TriggeredPerformanceCounters.ToArray()); } - /// <summary>Adds an alert.</summary> + /// <summary>Add an alert.</summary> /// <param name="executionTimeMilliseconds">The execution time in milliseconds.</param> /// <param name="thresholdMilliseconds">The configured threshold.</param> - /// <param name="alerts">The list of alert contexts.</param> - public void AddAlert(double executionTimeMilliseconds, double thresholdMilliseconds, List<AlertContext> alerts) + /// <param name="alerts">The sources involved in exceeding the threshold.</param> + public void AddAlert(double executionTimeMilliseconds, double thresholdMilliseconds, AlertContext[] alerts) { - this.PerformanceCounterManager.AddAlert(new AlertEntry(this, executionTimeMilliseconds, - thresholdMilliseconds, alerts)); + this.PerformanceMonitor.AddAlert( + new AlertEntry(this, executionTimeMilliseconds, thresholdMilliseconds, alerts) + ); } - /// <summary>Adds an alert for a single AlertContext</summary> + /// <summary>Add an alert.</summary> /// <param name="executionTimeMilliseconds">The execution time in milliseconds.</param> /// <param name="thresholdMilliseconds">The configured threshold.</param> - /// <param name="alert">The context</param> + /// <param name="alert">The source involved in exceeding the threshold.</param> public void AddAlert(double executionTimeMilliseconds, double thresholdMilliseconds, AlertContext alert) { - this.AddAlert(executionTimeMilliseconds, thresholdMilliseconds, new List<AlertContext>() {alert}); + this.AddAlert(executionTimeMilliseconds, thresholdMilliseconds, new[] { alert }); } - /// <summary>Resets the calls per second counter.</summary> + /// <summary>Reset the calls per second counter.</summary> public void ResetCallsPerSecond() { this.CallCount = 0; this.CallsPerSecondStart = DateTime.UtcNow; } - /// <summary>Resets all performance counters in this collection.</summary> + /// <summary>Reset all performance counters in this collection.</summary> public void Reset() { this.PeakInvocations.Clear(); - foreach (var i in this.PerformanceCounters) - i.Value.Reset(); + foreach (var counter in this.PerformanceCounters) + counter.Value.Reset(); } - /// <summary>Resets the performance counter for a specific source.</summary> - /// <param name="source">The source name</param> + /// <summary>Reset the performance counter for a specific source.</summary> + /// <param name="source">The source name.</param> public void ResetSource(string source) { foreach (var i in this.PerformanceCounters) @@ -219,15 +193,13 @@ namespace StardewModdingAPI.Framework.PerformanceCounter i.Value.Reset(); } - /// <summary>Returns the average calls per second.</summary> - /// <returns>The average calls per second.</returns> + /// <summary>Get the average calls per second.</summary> public long GetAverageCallsPerSecond() { - long runtimeInSeconds = (long) DateTime.UtcNow.Subtract(this.CallsPerSecondStart).TotalSeconds; - - if (runtimeInSeconds == 0) return 0; - - return this.CallCount / runtimeInSeconds; + long runtimeInSeconds = (long)DateTime.UtcNow.Subtract(this.CallsPerSecondStart).TotalSeconds; + return runtimeInSeconds > 0 + ? this.CallCount / runtimeInSeconds + : 0; } } } diff --git a/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterEntry.cs b/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterEntry.cs index a50fce7d..a1d78fc8 100644 --- a/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterEntry.cs +++ b/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterEntry.cs @@ -2,13 +2,29 @@ using System; namespace StardewModdingAPI.Framework.PerformanceCounter { - /// <summary>A single performance counter entry. Records the DateTime of the event and the elapsed millisecond.</summary> + /// <summary>A single performance counter entry.</summary> internal struct PerformanceCounterEntry { - /// <summary>The DateTime when the entry occured.</summary> - public DateTime EventTime; + /********* + ** Accessors + *********/ + /// <summary>When the entry occurred.</summary> + public DateTime EventTime { get; } - /// <summary>The elapsed milliseconds</summary> - public double ElapsedMilliseconds; + /// <summary>The elapsed milliseconds.</summary> + public double ElapsedMilliseconds { get; } + + + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> + /// <param name="eventTime">When the entry occurred.</param> + /// <param name="elapsedMilliseconds">The elapsed milliseconds.</param> + public PerformanceCounterEntry(DateTime eventTime, double elapsedMilliseconds) + { + this.EventTime = eventTime; + this.ElapsedMilliseconds = elapsedMilliseconds; + } } } diff --git a/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterManager.cs b/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterManager.cs index bd964442..81e4e468 100644 --- a/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterManager.cs +++ b/src/SMAPI/Framework/PerformanceCounter/PerformanceCounterManager.cs @@ -7,12 +7,14 @@ using StardewModdingAPI.Framework.Events; namespace StardewModdingAPI.Framework.PerformanceCounter { - internal class PerformanceCounterManager + /// <summary>Tracks performance metrics.</summary> + internal class PerformanceMonitor { - public HashSet<PerformanceCounterCollection> PerformanceCounterCollections = new HashSet<PerformanceCounterCollection>(); - + /********* + ** Fields + *********/ /// <summary>The recorded alerts.</summary> - private readonly List<AlertEntry> Alerts = new List<AlertEntry>(); + private readonly IList<AlertEntry> Alerts = new List<AlertEntry>(); /// <summary>The monitor for output logging.</summary> private readonly IMonitor Monitor; @@ -20,62 +22,64 @@ namespace StardewModdingAPI.Framework.PerformanceCounter /// <summary>The invocation stopwatch.</summary> private readonly Stopwatch InvocationStopwatch = new Stopwatch(); - /// <summary>Specifies if alerts should be paused.</summary> + /// <summary>The underlying performance counter collections.</summary> + private readonly IDictionary<string, PerformanceCounterCollection> Collections = new Dictionary<string, PerformanceCounterCollection>(StringComparer.InvariantCultureIgnoreCase); + + + /********* + ** Accessors + *********/ + /// <summary>Whether alerts are paused.</summary> public bool PauseAlerts { get; set; } - /// <summary>Specifies if performance counter tracking should be enabled.</summary> + /// <summary>Whether performance counter tracking is enabled.</summary> public bool EnableTracking { get; set; } - /// <summary>Constructs a performance counter manager.</summary> + + /********* + ** Public methods + *********/ + /// <summary>Construct an instance.</summary> /// <param name="monitor">The monitor for output logging.</param> - public PerformanceCounterManager(IMonitor monitor) + public PerformanceMonitor(IMonitor monitor) { this.Monitor = monitor; } - /// <summary>Resets all performance counters in all collections.</summary> + /// <summary>Reset all performance counters in all collections.</summary> public void Reset() { - foreach (PerformanceCounterCollection collection in this.PerformanceCounterCollections) - { + foreach (PerformanceCounterCollection collection in this.Collections.Values) collection.Reset(); - } - - foreach (var eventPerformanceCounter in - this.PerformanceCounterCollections.SelectMany(performanceCounter => performanceCounter.PerformanceCounters)) - { - eventPerformanceCounter.Value.Reset(); - } } - /// <summary>Begins tracking the invocation for a collection.</summary> - /// <param name="collectionName">The collection name</param> - public void BeginTrackInvocation(string collectionName) + /// <summary>Track the invocation time for a collection.</summary> + /// <param name="collectionName">The name of the collection.</param> + /// <param name="action">The action to execute and track.</param> + public void Track(string collectionName, Action action) { if (!this.EnableTracking) { + action(); return; } - this.GetOrCreateCollectionByName(collectionName).BeginTrackInvocation(); - } - - /// <summary>Ends tracking the invocation for a collection.</summary> - /// <param name="collectionName"></param> - public void EndTrackInvocation(string collectionName) - { - if (!this.EnableTracking) + PerformanceCounterCollection collection = this.GetOrCreateCollectionByName(collectionName); + collection.BeginTrackInvocation(); + try { - return; + action(); + } + finally + { + collection.EndTrackInvocation(); } - - this.GetOrCreateCollectionByName(collectionName).EndTrackInvocation(); } - /// <summary>Tracks a single performance counter invocation in a specific collection.</summary> + /// <summary>Track a single performance counter invocation in a specific collection.</summary> /// <param name="collectionName">The name of the collection.</param> /// <param name="sourceName">The name of the source.</param> - /// <param name="action">The action to execute and track invocation time for.</param> + /// <param name="action">The action to execute and track.</param> public void Track(string collectionName, string sourceName, Action action) { if (!this.EnableTracking) @@ -84,6 +88,7 @@ namespace StardewModdingAPI.Framework.PerformanceCounter return; } + PerformanceCounterCollection collection = this.GetOrCreateCollectionByName(collectionName); DateTime eventTime = DateTime.UtcNow; this.InvocationStopwatch.Reset(); this.InvocationStopwatch.Start(); @@ -95,79 +100,50 @@ namespace StardewModdingAPI.Framework.PerformanceCounter finally { this.InvocationStopwatch.Stop(); - - this.GetOrCreateCollectionByName(collectionName).Track(sourceName, new PerformanceCounterEntry - { - EventTime = eventTime, - ElapsedMilliseconds = this.InvocationStopwatch.Elapsed.TotalMilliseconds - }); + collection.Track(sourceName, new PerformanceCounterEntry(eventTime, this.InvocationStopwatch.Elapsed.TotalMilliseconds)); } } - /// <summary>Gets a collection by name.</summary> - /// <param name="name">The name of the collection.</param> - /// <returns>The collection or null if none was found.</returns> - private PerformanceCounterCollection GetCollectionByName(string name) - { - return this.PerformanceCounterCollections.FirstOrDefault(collection => collection.Name == name); - } - - /// <summary>Gets a collection by name and creates it if it doesn't exist.</summary> - /// <param name="name">The name of the collection.</param> - /// <returns>The collection.</returns> - private PerformanceCounterCollection GetOrCreateCollectionByName(string name) - { - PerformanceCounterCollection collection = this.GetCollectionByName(name); - - if (collection != null) return collection; - - collection = new PerformanceCounterCollection(this, name); - this.PerformanceCounterCollections.Add(collection); - - return collection; - } - - /// <summary>Resets the performance counters for a specific collection.</summary> + /// <summary>Reset the performance counters for a specific collection.</summary> /// <param name="name">The collection name.</param> public void ResetCollection(string name) { - foreach (PerformanceCounterCollection performanceCounterCollection in - this.PerformanceCounterCollections.Where(performanceCounterCollection => - performanceCounterCollection.Name.Equals(name, StringComparison.InvariantCultureIgnoreCase))) + if (this.Collections.TryGetValue(name, out PerformanceCounterCollection collection)) { - performanceCounterCollection.ResetCallsPerSecond(); - performanceCounterCollection.Reset(); + collection.ResetCallsPerSecond(); + collection.Reset(); } } - /// <summary>Resets performance counters for a specific source.</summary> + /// <summary>Reset performance counters for a specific source.</summary> /// <param name="name">The name of the source.</param> public void ResetSource(string name) { - foreach (PerformanceCounterCollection performanceCounterCollection in this.PerformanceCounterCollections) + foreach (PerformanceCounterCollection performanceCounterCollection in this.Collections.Values) performanceCounterCollection.ResetSource(name); } /// <summary>Print any queued alerts.</summary> public void PrintQueuedAlerts() { - if (this.Alerts.Count == 0) return; + if (this.Alerts.Count == 0) + return; - StringBuilder sb = new StringBuilder(); + StringBuilder report = new StringBuilder(); foreach (AlertEntry alert in this.Alerts) { - sb.AppendLine($"{alert.Collection.Name} took {alert.ExecutionTimeMilliseconds:F2}ms (exceeded threshold of {alert.ThresholdMilliseconds:F2}ms)"); + report.AppendLine($"{alert.Collection.Name} took {alert.ExecutionTimeMilliseconds:F2}ms (exceeded threshold of {alert.ThresholdMilliseconds:F2}ms)"); foreach (AlertContext context in alert.Context.OrderByDescending(p => p.Elapsed)) - sb.AppendLine(context.ToString()); + report.AppendLine(context.ToString()); } this.Alerts.Clear(); - this.Monitor.Log(sb.ToString(), LogLevel.Error); + this.Monitor.Log(report.ToString(), LogLevel.Error); } - /// <summary>Adds an alert to the queue.</summary> + /// <summary>Add an alert to the queue.</summary> /// <param name="entry">The alert to add.</param> public void AddAlert(AlertEntry entry) { @@ -175,68 +151,34 @@ namespace StardewModdingAPI.Framework.PerformanceCounter this.Alerts.Add(entry); } - /// <summary>Initialized the default performance counter collections.</summary> + /// <summary>Initialize the default performance counter collections.</summary> /// <param name="eventManager">The event manager.</param> public void InitializePerformanceCounterCollections(EventManager eventManager) { - this.PerformanceCounterCollections = new HashSet<PerformanceCounterCollection>() + foreach (IManagedEvent @event in eventManager.GetAllEvents()) + this.Collections[@event.EventName] = new PerformanceCounterCollection(this, @event.EventName, @event.IsPerformanceCritical); + } + + /// <summary>Get the underlying performance counters.</summary> + public IEnumerable<PerformanceCounterCollection> GetCollections() + { + return this.Collections.Values; + } + + + /********* + ** Public methods + *********/ + /// <summary>Get a collection by name and creates it if it doesn't exist.</summary> + /// <param name="name">The name of the collection.</param> + private PerformanceCounterCollection GetOrCreateCollectionByName(string name) + { + if (!this.Collections.TryGetValue(name, out PerformanceCounterCollection collection)) { - new EventPerformanceCounterCollection(this, eventManager.MenuChanged, false), - - // Rendering Events - new EventPerformanceCounterCollection(this, eventManager.Rendering, false), - new EventPerformanceCounterCollection(this, eventManager.Rendered, true), - new EventPerformanceCounterCollection(this, eventManager.RenderingWorld, false), - new EventPerformanceCounterCollection(this, eventManager.RenderedWorld, true), - new EventPerformanceCounterCollection(this, eventManager.RenderingActiveMenu, false), - new EventPerformanceCounterCollection(this, eventManager.RenderedActiveMenu, true), - new EventPerformanceCounterCollection(this, eventManager.RenderingHud, false), - new EventPerformanceCounterCollection(this, eventManager.RenderedHud, true), - - new EventPerformanceCounterCollection(this, eventManager.WindowResized, false), - new EventPerformanceCounterCollection(this, eventManager.GameLaunched, false), - new EventPerformanceCounterCollection(this, eventManager.UpdateTicking, true), - new EventPerformanceCounterCollection(this, eventManager.UpdateTicked, true), - new EventPerformanceCounterCollection(this, eventManager.OneSecondUpdateTicking, true), - new EventPerformanceCounterCollection(this, eventManager.OneSecondUpdateTicked, true), - - new EventPerformanceCounterCollection(this, eventManager.SaveCreating, false), - new EventPerformanceCounterCollection(this, eventManager.SaveCreated, false), - new EventPerformanceCounterCollection(this, eventManager.Saving, false), - new EventPerformanceCounterCollection(this, eventManager.Saved, false), - - new EventPerformanceCounterCollection(this, eventManager.DayStarted, false), - new EventPerformanceCounterCollection(this, eventManager.DayEnding, false), - - new EventPerformanceCounterCollection(this, eventManager.TimeChanged, true), - - new EventPerformanceCounterCollection(this, eventManager.ReturnedToTitle, false), - - new EventPerformanceCounterCollection(this, eventManager.ButtonPressed, true), - new EventPerformanceCounterCollection(this, eventManager.ButtonReleased, true), - new EventPerformanceCounterCollection(this, eventManager.CursorMoved, true), - new EventPerformanceCounterCollection(this, eventManager.MouseWheelScrolled, true), - - new EventPerformanceCounterCollection(this, eventManager.PeerContextReceived, false), - new EventPerformanceCounterCollection(this, eventManager.ModMessageReceived, false), - new EventPerformanceCounterCollection(this, eventManager.PeerDisconnected, false), - new EventPerformanceCounterCollection(this, eventManager.InventoryChanged, true), - new EventPerformanceCounterCollection(this, eventManager.LevelChanged, false), - new EventPerformanceCounterCollection(this, eventManager.Warped, false), - - new EventPerformanceCounterCollection(this, eventManager.LocationListChanged, false), - new EventPerformanceCounterCollection(this, eventManager.BuildingListChanged, false), - new EventPerformanceCounterCollection(this, eventManager.LocationListChanged, false), - new EventPerformanceCounterCollection(this, eventManager.DebrisListChanged, true), - new EventPerformanceCounterCollection(this, eventManager.LargeTerrainFeatureListChanged, true), - new EventPerformanceCounterCollection(this, eventManager.NpcListChanged, false), - new EventPerformanceCounterCollection(this, eventManager.ObjectListChanged, true), - new EventPerformanceCounterCollection(this, eventManager.ChestInventoryChanged, true), - new EventPerformanceCounterCollection(this, eventManager.TerrainFeatureListChanged, true), - new EventPerformanceCounterCollection(this, eventManager.LoadStageChanged, false), - new EventPerformanceCounterCollection(this, eventManager.UnvalidatedUpdateTicking, false), - new EventPerformanceCounterCollection(this, eventManager.UnvalidatedUpdateTicked, false), - }; + collection = new PerformanceCounterCollection(this, name); + this.Collections[name] = collection; + } + return collection; } } } |