using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; namespace StardewModdingAPI.Framework.PerformanceMonitoring { internal class PerformanceCounterCollection { /********* ** Fields *********/ /// The number of peak invocations to keep. private readonly int MaxEntries = 16384; /// The sources involved in exceeding alert thresholds. private readonly List TriggeredPerformanceCounters = new List(); /// The stopwatch used to track the invocation time. private readonly Stopwatch InvocationStopwatch = new Stopwatch(); /// The performance counter manager. private readonly PerformanceMonitor PerformanceMonitor; /// The time to calculate average calls per second. private DateTime CallsPerSecondStart = DateTime.UtcNow; /// The number of invocations. private long CallCount; /// The peak invocations. private readonly Stack PeakInvocations; /********* ** Accessors *********/ /// The associated performance counters. public IDictionary PerformanceCounters { get; } = new Dictionary(); /// The name of this collection. public string Name { get; } /// Whether the source is typically invoked at least once per second. public bool IsPerformanceCritical { get; } /// The alert threshold in milliseconds. public double AlertThresholdMilliseconds { get; set; } /// Whether alerts are enabled. public bool EnableAlerts { get; set; } /********* ** Public methods *********/ /// Construct an instance. /// The performance counter manager. /// The name of this collection. /// Whether the source is typically invoked at least once per second. public PerformanceCounterCollection(PerformanceMonitor performanceMonitor, string name, bool isPerformanceCritical = false) { this.PeakInvocations = new Stack(this.MaxEntries); this.Name = name; this.PerformanceMonitor = performanceMonitor; this.IsPerformanceCritical = isPerformanceCritical; } /// Track a single invocation for a named source. /// The name of the source. /// The entry. public void Track(string source, PerformanceCounterEntry entry) { // add entry if (!this.PerformanceCounters.ContainsKey(source)) this.PerformanceCounters.Add(source, new PerformanceCounter(this, source)); this.PerformanceCounters[source].Add(entry); // raise alert if (this.EnableAlerts) this.TriggeredPerformanceCounters.Add(new AlertContext(source, entry.ElapsedMilliseconds)); } /// Get the average execution time for all non-game internal sources in milliseconds. /// The interval for which to get the average, relative to now public double GetModsAverageExecutionTime(TimeSpan interval) { return this.PerformanceCounters .Where(entry => entry.Key != Constants.GamePerformanceCounterName) .Sum(entry => entry.Value.GetAverage(interval)); } /// Get the overall average execution time in milliseconds. /// The interval for which to get the average, relative to now public double GetAverageExecutionTime(TimeSpan interval) { return this.PerformanceCounters .Sum(entry => entry.Value.GetAverage(interval)); } /// Get the average execution time for game-internal sources in milliseconds. public double GetGameAverageExecutionTime(TimeSpan interval) { return this.PerformanceCounters.TryGetValue(Constants.GamePerformanceCounterName, out PerformanceCounter gameExecTime) ? gameExecTime.GetAverage(interval) : 0; } /// Get the peak execution time in milliseconds. /// The time range to search. /// The end time for the , or null for the current time. public double GetPeakExecutionTime(TimeSpan range, DateTime? endTime = null) { if (this.PeakInvocations.Count == 0) return 0; endTime ??= DateTime.UtcNow; DateTime startTime = endTime.Value.Subtract(range); return this.PeakInvocations .Where(entry => entry.EventTime >= startTime && entry.EventTime <= endTime) .OrderByDescending(x => x.ExecutionTimeMilliseconds) .Select(p => p.ExecutionTimeMilliseconds) .FirstOrDefault(); } /// Start tracking the invocation of this collection. public void BeginTrackInvocation() { this.TriggeredPerformanceCounters.Clear(); this.InvocationStopwatch.Reset(); this.InvocationStopwatch.Start(); this.CallCount++; } /// End tracking the invocation of this collection, and raise an alert if needed. public void EndTrackInvocation() { this.InvocationStopwatch.Stop(); // add invocation if (this.PeakInvocations.Count >= this.MaxEntries) this.PeakInvocations.Pop(); this.PeakInvocations.Push(new PeakEntry(this.InvocationStopwatch.Elapsed.TotalMilliseconds, DateTime.UtcNow, this.TriggeredPerformanceCounters.ToArray())); // raise alert if (this.EnableAlerts && this.InvocationStopwatch.Elapsed.TotalMilliseconds >= this.AlertThresholdMilliseconds) this.AddAlert(this.InvocationStopwatch.Elapsed.TotalMilliseconds, this.AlertThresholdMilliseconds, this.TriggeredPerformanceCounters.ToArray()); } /// Add an alert. /// The execution time in milliseconds. /// The configured threshold. /// The sources involved in exceeding the threshold. public void AddAlert(double executionTimeMilliseconds, double thresholdMilliseconds, AlertContext[] alerts) { this.PerformanceMonitor.AddAlert( new AlertEntry(this, executionTimeMilliseconds, thresholdMilliseconds, alerts) ); } /// Add an alert. /// The execution time in milliseconds. /// The configured threshold. /// The source involved in exceeding the threshold. public void AddAlert(double executionTimeMilliseconds, double thresholdMilliseconds, AlertContext alert) { this.AddAlert(executionTimeMilliseconds, thresholdMilliseconds, new[] { alert }); } /// Reset the calls per second counter. public void ResetCallsPerSecond() { this.CallCount = 0; this.CallsPerSecondStart = DateTime.UtcNow; } /// Reset all performance counters in this collection. public void Reset() { this.PeakInvocations.Clear(); foreach (var counter in this.PerformanceCounters) counter.Value.Reset(); } /// Reset the performance counter for a specific source. /// The source name. public void ResetSource(string source) { foreach (var i in this.PerformanceCounters) if (i.Value.Source.Equals(source, StringComparison.OrdinalIgnoreCase)) i.Value.Reset(); } /// Get the average calls per second. public long GetAverageCallsPerSecond() { long runtimeInSeconds = (long)DateTime.UtcNow.Subtract(this.CallsPerSecondStart).TotalSeconds; return runtimeInSeconds > 0 ? this.CallCount / runtimeInSeconds : 0; } } }