1 // Licensed to the .NET Foundation under one or more agreements. 2 // The .NET Foundation licenses this file to you under the MIT license. 3 // See the LICENSE file in the project root for more information. 4 5 using System.Collections; 6 using System.ComponentModel; 7 using System.Diagnostics; 8 9 namespace System.Internal 10 { 11 /// <summary> 12 /// The job of this class is to collect and track handle usage in windows forms. Ideally, a developer should never 13 /// have to call dispose() on any windows forms object. The problem in making this happen is in objects that are 14 /// very small to the VM garbage collector, but take up huge amounts of resources to the system. A good example of 15 /// this is a Win32 region handle. To the VM, a Region object is a small six ubyte object, so there isn't much need 16 /// to garbage collect it anytime soon. To Win32, however, a region handle consumes expensive USER and GDI 17 /// resources. Ideally we would like to be able to mark an object as "expensive" so it uses a different garbage 18 /// collection algorithm. In absence of that, we use the HandleCollector class, which runs a daemon thread to 19 /// garbage collect when handle usage goes up. 20 /// </summary> 21 internal class DebugHandleTracker 22 { 23 private static Hashtable s_handleTypes = new Hashtable(); 24 private static DebugHandleTracker s_tracker; 25 DebugHandleTracker()26 static DebugHandleTracker() 27 { 28 s_tracker = new DebugHandleTracker(); 29 30 if (CompModSwitches.HandleLeak.Level > TraceLevel.Off || CompModSwitches.TraceCollect.Enabled) 31 { 32 HandleCollector.HandleAdded += new HandleChangeEventHandler(s_tracker.OnHandleAdd); 33 HandleCollector.HandleRemoved += new HandleChangeEventHandler(s_tracker.OnHandleRemove); 34 } 35 } 36 DebugHandleTracker()37 private DebugHandleTracker() 38 { 39 } 40 41 private static object s_internalSyncObject = new object(); 42 43 /// <summary> 44 /// All handles available at this time will be not be considered as leaks when CheckLeaks is called to report leaks. 45 /// </summary> IgnoreCurrentHandlesAsLeaks()46 public static void IgnoreCurrentHandlesAsLeaks() 47 { 48 lock (s_internalSyncObject) 49 { 50 if (CompModSwitches.HandleLeak.Level >= TraceLevel.Warning) 51 { 52 HandleType[] types = new HandleType[s_handleTypes.Values.Count]; 53 s_handleTypes.Values.CopyTo(types, 0); 54 55 for (int i = 0; i < types.Length; i++) 56 { 57 types[i]?.IgnoreCurrentHandlesAsLeaks(); 58 } 59 } 60 } 61 } 62 63 /// <summary> 64 /// Called at shutdown to check for handles that are currently allocated. Normally, there should be none. 65 /// This will print a list of all handle leaks. 66 /// </summary> CheckLeaks()67 public static void CheckLeaks() 68 { 69 lock (s_internalSyncObject) 70 { 71 if (CompModSwitches.HandleLeak.Level >= TraceLevel.Warning) 72 { 73 GC.Collect(); 74 GC.WaitForPendingFinalizers(); 75 HandleType[] types = new HandleType[s_handleTypes.Values.Count]; 76 s_handleTypes.Values.CopyTo(types, 0); 77 78 Debug.WriteLine("------------Begin--CheckLeaks--------------------"); 79 for (int i = 0; i < types.Length; i++) 80 { 81 types[i]?.CheckLeaks(); 82 } 83 Debug.WriteLine("-------------End--CheckLeaks---------------------"); 84 } 85 } 86 } 87 88 /// <summary> 89 /// Ensures leak detection has been initialized. 90 /// </summary> Initialize()91 public static void Initialize() 92 { 93 // Calling this method forces the class to be loaded, thus running the 94 // static constructor which does all the work. 95 } 96 97 /// <summary> 98 /// Called by the Win32 handle collector when a new handle is created. 99 /// </summary> OnHandleAdd(string handleName, IntPtr handle, int handleCount)100 private void OnHandleAdd(string handleName, IntPtr handle, int handleCount) 101 { 102 HandleType type = (HandleType)s_handleTypes[handleName]; 103 if (type == null) 104 { 105 type = new HandleType(handleName); 106 s_handleTypes[handleName] = type; 107 } 108 type.Add(handle); 109 } 110 111 /// <summary> 112 /// Called by the Win32 handle collector when a new handle is created. 113 /// </summary> OnHandleRemove(string handleName, IntPtr handle, int HandleCount)114 private void OnHandleRemove(string handleName, IntPtr handle, int HandleCount) 115 { 116 HandleType type = (HandleType)s_handleTypes[handleName]; 117 118 bool removed = false; 119 if (type != null) 120 { 121 removed = type.Remove(handle); 122 } 123 124 if (!removed) 125 { 126 if (CompModSwitches.HandleLeak.Level >= TraceLevel.Error) 127 { 128 // It seems to me we shouldn't call HandleCollector.Remove more than once 129 // for a given handle, but we do just that for HWND's (NativeWindow.DestroyWindow 130 // and Control.WmNCDestroy). 131 Debug.WriteLine("*************************************************"); 132 Debug.WriteLine("While removing, couldn't find handle: " + Convert.ToString(unchecked((int)handle), 16)); 133 Debug.WriteLine("Handle Type : " + handleName); 134 Debug.WriteLine(Environment.StackTrace); 135 Debug.WriteLine("-------------------------------------------------"); 136 } 137 } 138 } 139 140 /// <summary> 141 /// Represents a specific type of handle. 142 /// </summary> 143 private class HandleType 144 { 145 public readonly string name; 146 147 private int _handleCount; 148 private HandleEntry[] _buckets; 149 150 private const int NumberOfBuckets = 10; 151 152 /// <summary> 153 /// Creates a new handle type. 154 /// </summary> HandleType(string name)155 public HandleType(string name) 156 { 157 this.name = name; 158 _buckets = new HandleEntry[NumberOfBuckets]; 159 } 160 161 /// <summary> 162 /// Adds a handle to this handle type for monitoring. 163 /// </summary> Add(IntPtr handle)164 public void Add(IntPtr handle) 165 { 166 lock (this) 167 { 168 int hash = ComputeHash(handle); 169 if (CompModSwitches.HandleLeak.Level >= TraceLevel.Info) 170 { 171 Debug.WriteLine("-------------------------------------------------"); 172 Debug.WriteLine("Handle Allocating: " + Convert.ToString(unchecked((int)handle), 16)); 173 Debug.WriteLine("Handle Type : " + name); 174 if (CompModSwitches.HandleLeak.Level >= TraceLevel.Verbose) 175 Debug.WriteLine(Environment.StackTrace); 176 } 177 178 HandleEntry entry = _buckets[hash]; 179 while (entry != null) 180 { 181 Debug.Assert(entry.handle != handle, "Duplicate handle of type " + name); 182 entry = entry.next; 183 } 184 185 _buckets[hash] = new HandleEntry(_buckets[hash], handle); 186 187 _handleCount++; 188 } 189 } 190 191 /// <summary> 192 /// Checks and reports leaks for handle monitoring. 193 /// </summary> CheckLeaks()194 public void CheckLeaks() 195 { 196 lock (this) 197 { 198 bool reportedFirstLeak = false; 199 if (_handleCount > 0) 200 { 201 for (int i = 0; i < NumberOfBuckets; i++) 202 { 203 HandleEntry e = _buckets[i]; 204 while (e != null) 205 { 206 if (!e.ignorableAsLeak) 207 { 208 if (!reportedFirstLeak) 209 { 210 Debug.WriteLine("\r\nHandle leaks detected for handles of type " + name + ":"); 211 reportedFirstLeak = true; 212 } 213 Debug.WriteLine(e.ToString(this)); 214 } 215 e = e.next; 216 } 217 } 218 } 219 } 220 } 221 222 /// <summary> 223 /// Marks all the handles currently stored, as ignorable, so that they will not be reported as leaks later. 224 /// </summary> IgnoreCurrentHandlesAsLeaks()225 public void IgnoreCurrentHandlesAsLeaks() 226 { 227 lock (this) 228 { 229 if (_handleCount > 0) 230 { 231 for (int i = 0; i < NumberOfBuckets; i++) 232 { 233 HandleEntry e = _buckets[i]; 234 while (e != null) 235 { 236 e.ignorableAsLeak = true; 237 e = e.next; 238 } 239 } 240 } 241 } 242 } 243 244 /// <summary> 245 /// Computes the hash bucket for this handle. 246 /// </summary> ComputeHash(IntPtr handle)247 private int ComputeHash(IntPtr handle) 248 { 249 return (unchecked((int)handle) & 0xFFFF) % NumberOfBuckets; 250 } 251 252 /// <summary> 253 /// Removes the given handle from our monitor list. 254 /// </summary> Remove(IntPtr handle)255 public bool Remove(IntPtr handle) 256 { 257 lock (this) 258 { 259 int hash = ComputeHash(handle); 260 if (CompModSwitches.HandleLeak.Level >= TraceLevel.Info) 261 { 262 Debug.WriteLine("-------------------------------------------------"); 263 Debug.WriteLine("Handle Releaseing: " + Convert.ToString(unchecked((int)handle), 16)); 264 Debug.WriteLine("Handle Type : " + name); 265 if (CompModSwitches.HandleLeak.Level >= TraceLevel.Verbose) 266 Debug.WriteLine(Environment.StackTrace); 267 } 268 HandleEntry e = _buckets[hash]; 269 HandleEntry last = null; 270 while (e != null && e.handle != handle) 271 { 272 last = e; 273 e = e.next; 274 } 275 if (e != null) 276 { 277 if (last == null) 278 { 279 _buckets[hash] = e.next; 280 } 281 else 282 { 283 last.next = e.next; 284 } 285 _handleCount--; 286 return true; 287 } 288 return false; 289 } 290 } 291 292 /// <summary> 293 /// Denotes a single entry in our handle list. 294 /// </summary> 295 private class HandleEntry 296 { 297 public readonly IntPtr handle; 298 public HandleEntry next; 299 public readonly string callStack; 300 public bool ignorableAsLeak; 301 302 /// <summary> 303 /// Creates a new handle entry 304 /// </summary> HandleEntry(HandleEntry next, IntPtr handle)305 public HandleEntry(HandleEntry next, IntPtr handle) 306 { 307 this.handle = handle; 308 this.next = next; 309 310 if (CompModSwitches.HandleLeak.Level > TraceLevel.Off) 311 { 312 callStack = Environment.StackTrace; 313 } 314 else 315 { 316 callStack = null; 317 } 318 } 319 320 /// <summary> 321 /// Converts this handle to a printable string. the string consists of the handle value along with 322 /// the callstack for it's allocation. 323 /// </summary> ToString(HandleType type)324 public string ToString(HandleType type) 325 { 326 StackParser sp = new StackParser(callStack); 327 328 // Discard all of the stack up to and including the "Handle.create" call 329 sp.DiscardTo("HandleCollector.Add"); 330 331 // Skip the next call as it is always a debug wrapper 332 sp.DiscardNext(); 333 334 // Now recreate the leak list with a lot of stack entries 335 sp.Truncate(40); 336 337 string description = ""; 338 339 return Convert.ToString(unchecked((int)handle), 16) + description + ": " + sp.ToString(); 340 } 341 342 /// <summary> 343 /// Simple stack parsing class to manipulate our callstack. 344 /// </summary> 345 private class StackParser 346 { 347 internal string releventStack; 348 internal int startIndex; 349 internal int endIndex; 350 internal int length; 351 352 /// <summary> 353 /// Creates a new stackparser with the given callstack 354 /// </summary> StackParser(string callStack)355 public StackParser(string callStack) 356 { 357 releventStack = callStack; 358 length = releventStack.Length; 359 } 360 361 /// <summary> 362 /// Determines if the given string contains token. This is a case sensitive match. 363 /// </summary> ContainsString(string str, string token)364 private static bool ContainsString(string str, string token) 365 { 366 int stringLength = str.Length; 367 int tokenLength = token.Length; 368 369 for (int s = 0; s < stringLength; s++) 370 { 371 int t = 0; 372 while (t < tokenLength && str[s + t] == token[t]) 373 { 374 t++; 375 } 376 if (t == tokenLength) 377 { 378 return true; 379 } 380 } 381 return false; 382 } 383 384 /// <summary> 385 /// Discards the next line of the stack trace. 386 /// </summary> DiscardNext()387 public void DiscardNext() 388 { 389 GetLine(); 390 } 391 392 /// <summary> 393 /// Discards all lines up to and including the line that contains discardText. 394 /// </summary> DiscardTo(string discardText)395 public void DiscardTo(string discardText) 396 { 397 while (startIndex < length) 398 { 399 string line = GetLine(); 400 if (line == null || ContainsString(line, discardText)) 401 { 402 break; 403 } 404 } 405 } 406 407 /// <summary> 408 /// Retrieves the next line of the stack. 409 /// </summary> GetLine()410 private string GetLine() 411 { 412 endIndex = releventStack.IndexOf('\r', startIndex); 413 if (endIndex < 0) 414 { 415 endIndex = length - 1; 416 } 417 418 string line = releventStack.Substring(startIndex, endIndex - startIndex); 419 char ch; 420 421 while (endIndex < length && ((ch = releventStack[endIndex]) == '\r' || ch == '\n')) 422 { 423 endIndex++; 424 } 425 if (startIndex == endIndex) return null; 426 startIndex = endIndex; 427 line = line.Replace('\t', ' '); 428 return line; 429 } 430 431 /// <summary> 432 /// Retrieves the string of the parsed stack trace 433 /// </summary> ToString()434 public override string ToString() 435 { 436 return releventStack.Substring(startIndex); 437 } 438 439 /// <summary> 440 /// Truncates the stack trace, saving the given # of lines. 441 /// </summary> Truncate(int lines)442 public void Truncate(int lines) 443 { 444 string truncatedStack = ""; 445 446 while (lines-- > 0 && startIndex < length) 447 { 448 if (truncatedStack == null) 449 { 450 truncatedStack = GetLine(); 451 } 452 else 453 { 454 truncatedStack += ": " + GetLine(); 455 } 456 truncatedStack += Environment.NewLine; 457 } 458 459 releventStack = truncatedStack; 460 startIndex = 0; 461 endIndex = 0; 462 length = releventStack.Length; 463 } 464 } 465 } 466 } 467 } 468 } 469