A game framework written with osu! in mind.
1// Copyright (c) ppy Pty Ltd <contact@ppy.sh>. Licensed under the MIT Licence.
2// See the LICENCE file in the repository root for full licence text.
3
4using System;
5using System.Collections.Concurrent;
6using System.Collections.Generic;
7using System.Threading;
8using Microsoft.Extensions.ObjectPool;
9using osu.Framework.Allocation;
10using osu.Framework.Bindables;
11using osu.Framework.Utils;
12using osu.Framework.Threading;
13using osu.Framework.Timing;
14
15namespace osu.Framework.Statistics
16{
17 internal class PerformanceMonitor : IDisposable
18 {
19 private readonly StopwatchClock ourClock = new StopwatchClock(true);
20
21 private readonly Stack<PerformanceCollectionType> currentCollectionTypeStack = new Stack<PerformanceCollectionType>();
22
23 private readonly InvokeOnDisposal[] endCollectionDelegates = new InvokeOnDisposal[FrameStatistics.NUM_PERFORMANCE_COLLECTION_TYPES];
24
25 private BackgroundStackTraceCollector traceCollector;
26
27 private FrameStatistics currentFrame;
28
29 private const int max_pending_frames = 10;
30
31 private readonly string threadName;
32
33 internal readonly ConcurrentQueue<FrameStatistics> PendingFrames = new ConcurrentQueue<FrameStatistics>();
34
35 internal readonly ObjectPool<FrameStatistics> FramesPool =
36 new DefaultObjectPoolProvider { MaximumRetained = max_pending_frames }
37 .Create(new DefaultPooledObjectPolicy<FrameStatistics>());
38
39 internal bool[] ActiveCounters { get; } = new bool[FrameStatistics.NUM_STATISTICS_COUNTER_TYPES];
40
41 private bool enablePerformanceProfiling;
42
43 public bool EnablePerformanceProfiling
44 {
45 set
46 {
47 enablePerformanceProfiling = value;
48 updateEnabledState();
49 }
50 }
51
52 private double consumptionTime;
53
54 private readonly IBindable<bool> isActive;
55
56 internal readonly ThrottledFrameClock Clock;
57
58 private Thread thread;
59
60 public double FrameAimTime => 1000.0 / (Clock?.MaximumUpdateHz > 0 ? Clock.MaximumUpdateHz : double.MaxValue);
61
62 internal PerformanceMonitor(GameThread thread, IEnumerable<StatisticsCounterType> counters)
63 {
64 Clock = thread.Clock;
65 threadName = thread.Name;
66
67 isActive = thread.IsActive.GetBoundCopy();
68 isActive.BindValueChanged(_ => updateEnabledState());
69
70 currentFrame = FramesPool.Get();
71
72 foreach (var c in counters)
73 ActiveCounters[(int)c] = true;
74
75 for (int i = 0; i < FrameStatistics.NUM_PERFORMANCE_COLLECTION_TYPES; i++)
76 {
77 var t = (PerformanceCollectionType)i;
78 endCollectionDelegates[i] = new InvokeOnDisposal(() => endCollecting(t));
79 }
80 }
81
82 /// <summary>
83 /// Switch target thread to <see cref="Thread.CurrentThread"/>.
84 /// </summary>
85 public void MakeCurrent()
86 {
87 var current = Thread.CurrentThread;
88
89 if (current == thread)
90 return;
91
92 thread = Thread.CurrentThread;
93
94 traceCollector?.Dispose();
95 traceCollector = new BackgroundStackTraceCollector(thread, ourClock, threadName);
96 updateEnabledState();
97 }
98
99 /// <summary>
100 /// Start collecting a type of passing time.
101 /// </summary>
102 public InvokeOnDisposal BeginCollecting(PerformanceCollectionType type)
103 {
104 if (currentCollectionTypeStack.Count > 0)
105 {
106 PerformanceCollectionType t = currentCollectionTypeStack.Peek();
107
108 if (!currentFrame.CollectedTimes.ContainsKey(t)) currentFrame.CollectedTimes[t] = 0;
109 currentFrame.CollectedTimes[t] += consumeStopwatchElapsedTime();
110 }
111
112 currentCollectionTypeStack.Push(type);
113
114 return endCollectionDelegates[(int)type];
115 }
116
117 /// <summary>
118 /// End collecting a type of passing time (that was previously started).
119 /// </summary>
120 /// <param name="type"></param>
121 private void endCollecting(PerformanceCollectionType type)
122 {
123 currentCollectionTypeStack.Pop();
124
125 if (!currentFrame.CollectedTimes.ContainsKey(type)) currentFrame.CollectedTimes[type] = 0;
126 currentFrame.CollectedTimes[type] += consumeStopwatchElapsedTime();
127 }
128
129 private readonly int[] lastAmountGarbageCollects = new int[3];
130
131 public bool HandleGC;
132
133 private readonly Dictionary<StatisticsCounterType, GlobalStatistic<long>> globalStatistics = new Dictionary<StatisticsCounterType, GlobalStatistic<long>>();
134
135 /// <summary>
136 /// Resets all frame statistics. Run exactly once per frame.
137 /// </summary>
138 public void NewFrame()
139 {
140 // Reset the counters we keep track of
141 for (int i = 0; i < ActiveCounters.Length; ++i)
142 {
143 if (ActiveCounters[i])
144 {
145 var count = FrameStatistics.COUNTERS[i];
146 var type = (StatisticsCounterType)i;
147
148 if (!globalStatistics.TryGetValue(type, out var global))
149 globalStatistics[type] = global = GlobalStatistics.Get<long>(threadName, type.ToString());
150
151 global.Value = count;
152 currentFrame.Counts[type] = count;
153 currentFrame.FramesPerSecond = Clock.FramesPerSecond;
154
155 FrameStatistics.COUNTERS[i] = 0;
156 }
157 }
158
159 if (PendingFrames.Count < max_pending_frames - 1)
160 {
161 PendingFrames.Enqueue(currentFrame);
162 currentFrame = FramesPool.Get();
163 }
164
165 currentFrame.Clear();
166
167 if (HandleGC)
168 {
169 for (int i = 0; i < lastAmountGarbageCollects.Length; ++i)
170 {
171 int amountCollections = GC.CollectionCount(i);
172
173 if (lastAmountGarbageCollects[i] != amountCollections)
174 {
175 lastAmountGarbageCollects[i] = amountCollections;
176 currentFrame.GarbageCollections.Add(i);
177 }
178 }
179 }
180
181 double dampRate = Math.Max(Clock.ElapsedFrameTime, 0) / 1000;
182 averageFrameTime = Interpolation.Damp(averageFrameTime, Clock.ElapsedFrameTime, 0.01, dampRate);
183
184 //check for dropped (stutter) frames
185 traceCollector?.NewFrame(Clock.ElapsedFrameTime, Math.Max(10, Math.Max(1000 / Clock.MaximumUpdateHz, averageFrameTime) * 4));
186
187 consumeStopwatchElapsedTime();
188 }
189
190 public void EndFrame()
191 {
192 traceCollector?.EndFrame();
193 }
194
195 private void updateEnabledState()
196 {
197 if (traceCollector != null)
198 traceCollector.Enabled = enablePerformanceProfiling && isActive.Value;
199 }
200
201 private double averageFrameTime;
202
203 private double consumeStopwatchElapsedTime()
204 {
205 double last = consumptionTime;
206
207 consumptionTime = ourClock.CurrentTime;
208 if (traceCollector != null)
209 traceCollector.LastConsumptionTime = consumptionTime;
210
211 return consumptionTime - last;
212 }
213
214 internal double FramesPerSecond => Clock.FramesPerSecond;
215
216 #region IDisposable Support
217
218 private bool isDisposed;
219
220 protected virtual void Dispose(bool disposing)
221 {
222 if (!isDisposed)
223 {
224 isDisposed = true;
225 traceCollector?.Dispose();
226 }
227 }
228
229 public void Dispose()
230 {
231 Dispose(true);
232 GC.SuppressFinalize(this);
233 }
234
235 #endregion
236 }
237}