A game framework written with osu! in mind.
at master 232 lines 7.6 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.Diagnostics; 6using System.Linq; 7using System.Text; 8using System.Threading; 9using Microsoft.Diagnostics.Runtime; 10using osu.Framework.Logging; 11using osu.Framework.Timing; 12 13namespace osu.Framework.Statistics 14{ 15 /// <summary> 16 /// Spawn a thread to collect real-time stack traces of the targeted thread. 17 /// </summary> 18 internal class BackgroundStackTraceCollector : IDisposable 19 { 20 private string[] backgroundMonitorStackTrace; 21 22 private readonly StopwatchClock clock; 23 private readonly string threadName; 24 25 private readonly Lazy<Logger> logger; 26 27 private Thread targetThread; 28 29 internal double LastConsumptionTime; 30 31 private double spikeRecordThreshold; 32 33 private bool enabled; 34 35 /// <summary> 36 /// Create a collector for the target thread. Starts in a disabled state (see <see cref="Enabled"/>. 37 /// </summary> 38 /// <param name="targetThread">The thread to monitor.</param> 39 /// <param name="clock">The clock to use for elapsed time checks.</param> 40 /// <param name="threadName">A name used for tracking purposes. Can be used to track potentially changing threads under a single name.</param> 41 public BackgroundStackTraceCollector(Thread targetThread, StopwatchClock clock, string threadName = null) 42 { 43 if (Debugger.IsAttached) 44 return; 45 46 this.clock = clock; 47 this.threadName = threadName ?? targetThread?.Name; 48 this.targetThread = targetThread; 49 50 logger = new Lazy<Logger>(() => 51 { 52 var l = Logger.GetLogger($"performance-{threadName?.ToLower() ?? "unknown"}"); 53 l.OutputToListeners = false; 54 return l; 55 }); 56 } 57 58 /// <summary> 59 /// Whether this collector is currently running. 60 /// </summary> 61 public bool Enabled 62 { 63 get => enabled; 64 set 65 { 66 if (value == enabled || targetThread == null) return; 67 68 enabled = value; 69 if (enabled) 70 startThread(); 71 else 72 stopThread(); 73 } 74 } 75 76 private CancellationTokenSource cancellation; 77 78 private void startThread() 79 { 80 // Since v2.0 of Microsoft.Diagnostics.Runtime, support is provided to retrieve stack traces on unix platforms but 81 // it causes a full core dump, which is very slow and causes a visible freeze. 82 // For the time being let's remain windows-only (as this functionality used to be). 83 84 // As it turns out, it's also too slow to be useful on windows, so let's fully disable for the time being. 85 86 //if (RuntimeInfo.OS != RuntimeInfo.Platform.Windows) 87 // return; 88 89 /*Trace.Assert(cancellation == null); 90 91 var thread = new Thread(() => run((cancellation = new CancellationTokenSource()).Token)) 92 { 93 Name = $"{threadName}-StackTraceCollector", 94 IsBackground = true 95 }; 96 97 thread.Start();*/ 98 } 99 100 private bool isCollecting; 101 102 private void run(CancellationToken cancellation) 103 { 104 while (!cancellation.IsCancellationRequested) 105 { 106 var elapsed = clock.ElapsedMilliseconds - LastConsumptionTime; 107 var threshold = spikeRecordThreshold / 2; 108 109 if (targetThread.IsAlive && isCollecting && clock.ElapsedMilliseconds - LastConsumptionTime > spikeRecordThreshold / 2 && backgroundMonitorStackTrace == null) 110 { 111 try 112 { 113 Logger.Log($"Retrieving background stack trace on {threadName} thread ({elapsed:N0}ms over threshold of {threshold:N0}ms)..."); 114 backgroundMonitorStackTrace = getStackTrace(targetThread); 115 Logger.Log("Done!"); 116 117 Thread.Sleep(100); 118 } 119 catch (Exception e) 120 { 121 Enabled = false; 122 Logger.Log($"Failed to retrieve background stack trace: {e}"); 123 } 124 } 125 126 Thread.Sleep(5); 127 } 128 } 129 130 private void stopThread() 131 { 132 cancellation?.Cancel(); 133 cancellation?.Dispose(); 134 cancellation = null; 135 } 136 137 internal void NewFrame(double elapsedFrameTime, double newSpikeThreshold) 138 { 139 if (targetThread == null) return; 140 141 isCollecting = true; 142 143 var frames = backgroundMonitorStackTrace; 144 backgroundMonitorStackTrace = null; 145 146 var currentThreshold = spikeRecordThreshold; 147 148 spikeRecordThreshold = newSpikeThreshold; 149 150 if (!enabled || elapsedFrameTime < currentThreshold || currentThreshold == 0) 151 return; 152 153 StringBuilder logMessage = new StringBuilder(); 154 155 logMessage.AppendLine($@"| Slow frame on thread ""{threadName}"""); 156 logMessage.AppendLine(@"|"); 157 logMessage.AppendLine($@"| * Thread time : {clock.CurrentTime:#0,#}ms"); 158 logMessage.AppendLine($@"| * Frame length : {elapsedFrameTime:#0,#}ms (allowable: {currentThreshold:#0,#}ms)"); 159 160 logMessage.AppendLine(@"|"); 161 162 if (frames != null) 163 { 164 logMessage.AppendLine(@"| Stack trace:"); 165 166 foreach (var f in frames) 167 logMessage.AppendLine($@"|- {f}"); 168 } 169 else 170 logMessage.AppendLine(@"| Call stack was not recorded."); 171 172 logger.Value.Add(logMessage.ToString()); 173 } 174 175 public void EndFrame() 176 { 177 isCollecting = false; 178 } 179 180 private static string[] getStackTrace(Thread targetThread) 181 { 182 try 183 { 184#if NET5_0 185 using (var target = DataTarget.CreateSnapshotAndAttach(Environment.ProcessId)) 186#else 187 using (var target = DataTarget.CreateSnapshotAndAttach(Process.GetCurrentProcess().Id)) 188#endif 189 { 190 using (var runtime = target.ClrVersions[0].CreateRuntime()) 191 { 192 return runtime.Threads 193 .FirstOrDefault(t => t.ManagedThreadId == targetThread.ManagedThreadId)? 194 .EnumerateStackTrace().Select(f => f.ToString()) 195 .ToArray(); 196 } 197 } 198 } 199 catch 200 { 201 return null; 202 } 203 } 204 205 #region IDisposable Support 206 207 ~BackgroundStackTraceCollector() 208 { 209 Dispose(false); 210 } 211 212 private bool isDisposed; 213 214 protected virtual void Dispose(bool disposing) 215 { 216 if (!isDisposed) 217 { 218 Enabled = false; // stops the thread if running. 219 isDisposed = true; 220 targetThread = null; 221 } 222 } 223 224 public void Dispose() 225 { 226 Dispose(true); 227 GC.SuppressFinalize(this); 228 } 229 230 #endregion 231 } 232}