summaryrefslogtreecommitdiff
path: root/src/SMAPI/Framework/PerformanceMonitoring/PerformanceCounterCollection.cs
blob: 0bb78c7485c1c179db9f05d0d6f24063e430cfba (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;

namespace StardewModdingAPI.Framework.PerformanceMonitoring
{
    internal class PerformanceCounterCollection
    {
        /*********
        ** Fields
        *********/
        /// <summary>The number of peak invocations to keep.</summary>
        private readonly int MaxEntries = 16384;

        /// <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 PerformanceMonitor PerformanceMonitor;

        /// <summary>The time to calculate average calls per second.</summary>
        private DateTime CallsPerSecondStart = DateTime.UtcNow;

        /// <summary>The number of invocations.</summary>
        private long CallCount;

        /// <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>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>Whether alerts are enabled.</summary>
        public bool EnableAlerts { get; set; }


        /*********
        ** 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 Stack<PeakEntry>(this.MaxEntries);
            this.Name = name;
            this.PerformanceMonitor = performanceMonitor;
            this.IsPerformanceCritical = isPerformanceCritical;
        }

        /// <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>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>
        public double GetModsAverageExecutionTime(TimeSpan interval)
        {
            return this.PerformanceCounters
                .Where(entry => entry.Key != Constants.GamePerformanceCounterName)
                .Sum(entry => entry.Value.GetAverage(interval));
        }

        /// <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>
        public double GetAverageExecutionTime(TimeSpan interval)
        {
            return this.PerformanceCounters
                .Sum(entry => entry.Value.GetAverage(interval));
        }

        /// <summary>Get the average execution time for game-internal sources in milliseconds.</summary>
        public double GetGameAverageExecutionTime(TimeSpan interval)
        {
            return this.PerformanceCounters.TryGetValue(Constants.GamePerformanceCounterName, out PerformanceCounter gameExecTime)
                ? gameExecTime.GetAverage(interval)
                : 0;
        }

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

        /// <summary>Start tracking the invocation of this collection.</summary>
        public void BeginTrackInvocation()
        {
            this.TriggeredPerformanceCounters.Clear();
            this.InvocationStopwatch.Reset();
            this.InvocationStopwatch.Start();

            this.CallCount++;
        }

        /// <summary>End tracking the invocation of this collection, and raise an alert if needed.</summary>
        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());
        }

        /// <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 sources involved in exceeding the threshold.</param>
        public void AddAlert(double executionTimeMilliseconds, double thresholdMilliseconds, AlertContext[] alerts)
        {
            this.PerformanceMonitor.AddAlert(
                new AlertEntry(this, executionTimeMilliseconds, thresholdMilliseconds, alerts)
            );
        }

        /// <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 source involved in exceeding the threshold.</param>
        public void AddAlert(double executionTimeMilliseconds, double thresholdMilliseconds, AlertContext alert)
        {
            this.AddAlert(executionTimeMilliseconds, thresholdMilliseconds, new[] { alert });
        }

        /// <summary>Reset the calls per second counter.</summary>
        public void ResetCallsPerSecond()
        {
            this.CallCount = 0;
            this.CallsPerSecondStart = DateTime.UtcNow;
        }

        /// <summary>Reset all performance counters in this collection.</summary>
        public void Reset()
        {
            this.PeakInvocations.Clear();
            foreach (var counter in this.PerformanceCounters)
                counter.Value.Reset();
        }

        /// <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)
                if (i.Value.Source.Equals(source, StringComparison.InvariantCultureIgnoreCase))
                    i.Value.Reset();
        }

        /// <summary>Get the average calls per second.</summary>
        public long GetAverageCallsPerSecond()
        {
            long runtimeInSeconds = (long)DateTime.UtcNow.Subtract(this.CallsPerSecondStart).TotalSeconds;
            return runtimeInSeconds > 0
                ? this.CallCount / runtimeInSeconds
                : 0;
        }
    }
}