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.InvariantCultureIgnoreCase))
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;
}
}
}