using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using Cyotek.Collections.Generic; namespace StardewModdingAPI.Framework.PerformanceCounter { internal class PerformanceCounterCollection { /// The size of the ring buffer. private const int MAX_ENTRIES = 16384; /// The list of triggered performance counters. private readonly List TriggeredPerformanceCounters = new List(); /// The stopwatch used to track the invocation time. private readonly Stopwatch InvocationStopwatch = new Stopwatch(); /// The performance counter manager. private readonly PerformanceCounterManager PerformanceCounterManager; /// Holds the time to calculate the average calls per second. private DateTime CallsPerSecondStart = DateTime.UtcNow; /// The number of invocations of this collection. private long CallCount; /// The circular buffer which stores all peak invocations private readonly CircularBuffer PeakInvocations; /// The associated performance counters. public IDictionary PerformanceCounters { get; } = new Dictionary(); /// The name of this collection. public string Name { get; } /// Flag if this collection is important (used for the console summary command). public bool IsImportant { get; } /// The alert threshold in milliseconds. public double AlertThresholdMilliseconds { get; set; } /// If alerting is enabled or not public bool EnableAlerts { get; set; } public PerformanceCounterCollection(PerformanceCounterManager performanceCounterManager, string name, bool isImportant) { this.PeakInvocations = new CircularBuffer(PerformanceCounterCollection.MAX_ENTRIES); this.Name = name; this.PerformanceCounterManager = performanceCounterManager; this.IsImportant = isImportant; } public PerformanceCounterCollection(PerformanceCounterManager performanceCounterManager, string name) { this.PeakInvocations = new CircularBuffer(PerformanceCounterCollection.MAX_ENTRIES); this.PerformanceCounterManager = performanceCounterManager; this.Name = name; } /// Tracks a single invocation for a named source. /// The name of the source. /// The entry. public void Track(string source, PerformanceCounterEntry entry) { if (!this.PerformanceCounters.ContainsKey(source)) this.PerformanceCounters.Add(source, new PerformanceCounter(this, source)); this.PerformanceCounters[source].Add(entry); if (this.EnableAlerts) this.TriggeredPerformanceCounters.Add(new AlertContext(source, entry.ElapsedMilliseconds)); } /// Returns the average execution time for all non-game internal sources. /// The average execution time in milliseconds public double GetModsAverageExecutionTime() { return this.PerformanceCounters.Where(p => p.Key != Constants.GamePerformanceCounterName).Sum(p => p.Value.GetAverage()); } /// Returns the average execution time for all non-game internal sources. /// The interval for which to get the average, relative to now /// The average execution time in milliseconds public double GetModsAverageExecutionTime(TimeSpan interval) { return this.PerformanceCounters.Where(p => p.Key != Constants.GamePerformanceCounterName).Sum(p => p.Value.GetAverage(interval)); } /// Returns the overall average execution time. /// The average execution time in milliseconds public double GetAverageExecutionTime() { return this.PerformanceCounters.Sum(p => p.Value.GetAverage()); } /// Returns the overall average execution time. /// The interval for which to get the average, relative to now /// The average execution time in milliseconds public double GetAverageExecutionTime(TimeSpan interval) { return this.PerformanceCounters.Sum(p => p.Value.GetAverage(interval)); } /// Returns the average execution time for game-internal sources. /// The average execution time in milliseconds public double GetGameAverageExecutionTime() { if (this.PerformanceCounters.TryGetValue(Constants.GamePerformanceCounterName, out PerformanceCounter gameExecTime)) return gameExecTime.GetAverage(); return 0; } /// Returns the average execution time for game-internal sources. /// The average execution time in milliseconds public double GetGameAverageExecutionTime(TimeSpan interval) { if (this.PerformanceCounters.TryGetValue(Constants.GamePerformanceCounterName, out PerformanceCounter gameExecTime)) return gameExecTime.GetAverage(interval); return 0; } /// Returns the peak execution time /// The interval for which to get the peak, relative to /// The DateTime which the is relative to, or DateTime.Now if not given /// The peak execution time public double GetPeakExecutionTime(TimeSpan range, DateTime? relativeTo = null) { if (this.PeakInvocations.IsEmpty) return 0; if (relativeTo == null) relativeTo = DateTime.UtcNow; DateTime start = relativeTo.Value.Subtract(range); var entries = this.PeakInvocations.Where(x => (x.EventTime >= start) && (x.EventTime <= relativeTo)).ToList(); if (!entries.Any()) return 0; return entries.OrderByDescending(x => x.ExecutionTimeMilliseconds).First().ExecutionTimeMilliseconds; } /// Begins tracking the invocation of this collection. public void BeginTrackInvocation() { this.TriggeredPerformanceCounters.Clear(); this.InvocationStopwatch.Reset(); this.InvocationStopwatch.Start(); this.CallCount++; } /// Ends tracking the invocation of this collection. Also records an alert if alerting is enabled /// and the invocation time exceeds the threshold. public void EndTrackInvocation() { this.InvocationStopwatch.Stop(); this.PeakInvocations.Put( new PeakEntry(this.InvocationStopwatch.Elapsed.TotalMilliseconds, DateTime.UtcNow, this.TriggeredPerformanceCounters)); if (!this.EnableAlerts) return; if (this.InvocationStopwatch.Elapsed.TotalMilliseconds >= this.AlertThresholdMilliseconds) this.AddAlert(this.InvocationStopwatch.Elapsed.TotalMilliseconds, this.AlertThresholdMilliseconds, this.TriggeredPerformanceCounters); } /// Adds an alert. /// The execution time in milliseconds. /// The configured threshold. /// The list of alert contexts. public void AddAlert(double executionTimeMilliseconds, double thresholdMilliseconds, List alerts) { this.PerformanceCounterManager.AddAlert(new AlertEntry(this, executionTimeMilliseconds, thresholdMilliseconds, alerts)); } /// Adds an alert for a single AlertContext /// The execution time in milliseconds. /// The configured threshold. /// The context public void AddAlert(double executionTimeMilliseconds, double thresholdMilliseconds, AlertContext alert) { this.AddAlert(executionTimeMilliseconds, thresholdMilliseconds, new List() {alert}); } /// Resets the calls per second counter. public void ResetCallsPerSecond() { this.CallCount = 0; this.CallsPerSecondStart = DateTime.UtcNow; } /// Resets all performance counters in this collection. public void Reset() { this.PeakInvocations.Clear(); foreach (var i in this.PerformanceCounters) i.Value.Reset(); } /// Resets the performance counter for a specific source. /// The source name public void ResetSource(string source) { foreach (var i in this.PerformanceCounters) if (i.Value.Source.Equals(source, StringComparison.InvariantCultureIgnoreCase)) i.Value.Reset(); } /// Returns the average calls per second. /// The average calls per second. public long GetAverageCallsPerSecond() { long runtimeInSeconds = (long) DateTime.UtcNow.Subtract(this.CallsPerSecondStart).TotalSeconds; if (runtimeInSeconds == 0) return 0; return this.CallCount / runtimeInSeconds; } } }