A game framework written with osu! in mind.
at master 237 lines 7.9 kB view raw
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}