using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; namespace StardewModdingAPI.Framework.PerformanceCounter { internal class PerformanceCounterCollection { /// <summary>The list of triggered performance counters.</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; /// <summary>Holds the time to calculate the average calls per second.</summary> private DateTime CallsPerSecondStart = DateTime.UtcNow; /// <summary>The number of invocations of this collection.</summary> private long CallCount; 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>The alert threshold in milliseconds.</summary> public double AlertThresholdMilliseconds { get; set; } /// <summary>If alerting is enabled or not</summary> public bool EnableAlerts { get; set; } 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; } /// <summary>Tracks 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) { 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)); } /// <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 overall average execution time.</summary> /// <returns>The average execution time in milliseconds</returns> public double GetAverageExecutionTime() { return this.PerformanceCounters.Sum(p => p.Value.GetAverage()); } /// <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>Begins tracking the invocation of this collection.</summary> public void BeginTrackInvocation() { if (this.EnableAlerts) { this.TriggeredPerformanceCounters.Clear(); this.InvocationStopwatch.Reset(); this.InvocationStopwatch.Start(); } 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> public void EndTrackInvocation() { if (!this.EnableAlerts) return; this.InvocationStopwatch.Stop(); if (this.InvocationStopwatch.Elapsed.TotalMilliseconds >= this.AlertThresholdMilliseconds) this.AddAlert(this.InvocationStopwatch.Elapsed.TotalMilliseconds, this.AlertThresholdMilliseconds, this.TriggeredPerformanceCounters); } /// <summary>Adds 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) { this.PerformanceCounterManager.AddAlert(new AlertEntry(this, executionTimeMilliseconds, thresholdMilliseconds, alerts)); } /// <summary>Adds an alert for a single AlertContext</summary> /// <param name="executionTimeMilliseconds">The execution time in milliseconds.</param> /// <param name="thresholdMilliseconds">The configured threshold.</param> /// <param name="alert">The context</param> public void AddAlert(double executionTimeMilliseconds, double thresholdMilliseconds, AlertContext alert) { this.AddAlert(executionTimeMilliseconds, thresholdMilliseconds, new List<AlertContext>() {alert}); } /// <summary>Resets 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> public void Reset() { foreach (var i in this.PerformanceCounters) i.Value.Reset(); } /// <summary>Resets 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) if (i.Value.Source.Equals(source, StringComparison.InvariantCultureIgnoreCase)) i.Value.Reset(); } /// <summary>Returns the average calls per second.</summary> /// <returns>The average calls per second.</returns> public long GetAverageCallsPerSecond() { long runtimeInSeconds = (long) DateTime.UtcNow.Subtract(this.CallsPerSecondStart).TotalSeconds; if (runtimeInSeconds == 0) return 0; return this.CallCount / runtimeInSeconds; } } }