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.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}