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