Windbg and SOS for Visual Studio .NET Weenies

by breeve 10. February 2010 16:07

I first realized I was a Visual Studio weenie when I showed up to a Mono conference in Spain with a Windows laptop in a sea of Linux ones. I was so outnumbered I entertained the idea of not booting the Windows laptop at all because having the familiar Windows boot tone echoing off the conference walls would undoubtedly gather unwanted attention. Eventually, I calmed down and even managed to boot my Windows machine without too many stares.

One of the talks at the Mono conference—Mono is an open source implementation of .NET for Linux based machines—was about the progress on the Mono .NET debugger. Mono had released without a debugger initially and the debugger quickly become the #1 requested feature, no doubt mostly from .NET Windows users looking to port their .NET apps to Linux. The debugger being worked on, the presenter informed us, would not just be a command line debugger like GDB but would be fully integrated into MonoDevelop—the Linux equivalent of a Visual Studio—so that breakpoints could be set next to the source code. He even had a cute demo that worked. This is nice, I thought. After all, who needs a complicated command line debugger that takes years to learn. I needed to get things done.

During the many breaks we had between presentations, I was able to sit next to a few of the Mono developers and watch them code. They were very efficient with GDB and VI—the command line debugger and editor of choice for any self respecting Linux hacker. Still I figured, I was just as productive in Visual Studio.

And for the most part I am, except for nasty programming realities like memory leaks. For those, Visual Studio was at a loss and I was too until I discovered Windbg, the command line debugger for Windows, and its extension SOS. After learning a handful of commands, I was impressed in how quickly I was able to track down the source of leaks that had eluded fellow Visual Studio zealots. Maybe those Mono guys were on to something. After all, nothing says I am a debugger extraordinaire quicker than opening up Windbg and pounding out a few commands in front of your fellow programmers.

Below is a few commands and a tutorial on tracking down a leak in an example program.

!dumpheap -stat            Displays every managed type on the heap

!dumpheap -type typename   Displays every managed type that matches the specified type

!dumpheap -mt methodtable  Displays every instance of the specified method table

!gcroot address            Shows reference chain

!dumpobj address           Shows fields and addresses of references

!dumparray address         Shows objects in an array

!dumpmd                    Dumps the method description

!u                         Show assembly code

g                          Continue, let the program run

Ctrl+Break                 Break into the Windbg debugger

!help [command]            Show help

The .NET Windows Forms example program I wrote draws circles on a form. The circles are placed in random locations and move from top to bottom until they vanish. There are 500 circles on the form at any time. The program works well except it leaks memory like the Titanic. It went from 10MB to 20MB in 30 seconds and climbs into the hundreds of megs if left to run.

The code is below. See if you can spot the issue before we run Windbg.

public partial class Form1 : Form
{
    private const int NumberOfCircles = 500;
    private Timer _timer;

    public event EventHandler<DrawCircleEventArgs> Draw;

    public Form1()
    {
        InitializeComponent();

        _timer = new Timer();
        _timer.Interval = 100;
        _timer.Start();
        _timer.Tick += new EventHandler(OnTick);
        InitCircles();
    }

    private void InitCircles()
    {
        for (int x = 0; x < NumberOfCircles; x++)
            AddCircle();
    }

    private void AddCircle()
    {
        Circle circle = new Circle(new Rectangle(0, 0, Width, Height));
        circle.Done += new EventHandler(OnDone);
        Draw += new EventHandler<DrawCircleEventArgs>(circle.Draw);
    }

    private void OnTick(object sender, EventArgs e)
    {
        Invalidate();
    }

    protected override void OnPaint(PaintEventArgs e)
    {
        if (Draw != null)
            Draw(this, new DrawCircleEventArgs(e.Graphics));
    }

    private void OnDone(object sender, EventArgs e)
    {
        AddCircle();
    }
}

public class Circle
{
    private static Random Rand;

    private Rectangle _parentBounds;
    private Point _position;
    private Size _circleSize;
    private byte[] _bytes;
    private bool _done;
    public event EventHandler Done;

    static Circle()
    {
        Rand = new Random((int)DateTime.Now.Ticks);
    }

    public Circle(Rectangle parentBounds)
    {
        _bytes = new byte[10240]; //10K for no reason but to make object bigger
        _done = false;
        _parentBounds = parentBounds;
        int xPosition = Rand.Next(parentBounds.Left, parentBounds.Right);
        int yPosition = Rand.Next(parentBounds.Top, parentBounds.Bottom);
        _position = new Point(xPosition, yPosition);
        _circleSize = new Size(10, 10);
    }

    private void OnDone()
    {
        _done = true;
        if (Done != null)
            Done(this, EventArgs.Empty);
    }

    public void Draw(object sender, DrawCircleEventArgs args)
    {
        _position = new Point(_position.X, _position.Y + 1);
        Rectangle rect = new Rectangle(_position, _circleSize);

        if (!_parentBounds.Contains(rect) && !_done)
            OnDone();
        else
            args.Graphics.DrawEllipse(Pens.Green, rect);
    }
}

To find the leak, I will open Windbg and attach to the program using File>>Attach to Process. If you don’t have Windbg, download the debugging tools for windows here. Next, load SOS like:

.loadby sos mscorwks

With SOS loaded, I can run commands from above like:

!dumpheap -stat

658551a8      163         3912 System.Windows.Forms.InvalidateEventArgs
641b8c4c       95         5320 System.Configuration.FactoryRecord
6585394c       78         5616 System.Windows.Forms.Internal.DeviceContext
6a72a930      190         6840 System.Collections.Hashtable+HashtableEnumerator
6a73303c       28         7608 System.Collections.Hashtable+bucket[]
6a7308ec      812        61060 System.String
6a7040bc      153        63636 System.Object[]
6a729b58     4804       153728 System.EventHandler
00126ba8     4847       155104 System.EventHandler`1[[Leaky.DrawCircleEventArgs, Leaky]]
00126ac4     4725       245700 Leaky.Circle
0032c6b0     1754       279984      Free
6a73335c     4729     48451204 System.Byte[]

This shows all types on the heap, number of instances, and the sizes with the largest on the bottom. I can see the byte arrays are taking 48 Megs and Leaky.Circle is 245k. The large byte arrays are a field of the Circle class designed to show the leak easier. What is more interesting is there are 4725 instances of the circle class. I was expecting only around 500 because as the circles go outside the form they should be garbage collected. So what is causing these Circle instances to stay in memory? I dump the method table listed for the Circle like:

!dumpheap -mt 00126ac4

071deb54 00126ac4       52    
071e13f4 00126ac4       52    
071e40fc 00126ac4       52    
071e699c 00126ac4       52    
071e923c 00126ac4       52    
071ebadc 00126ac4       52    
071ee37c 00126ac4       52

This shows all the instances of that method table on the heap. I can now use !gcroot on one of the addresses like:

!gcroot 071e13f4

ebx:Root:01c842b4(System.Windows.Forms.Application+ThreadContext)->
01c83878(Leaky.Form1)->
071f0bfc(System.EventHandler`1[[Leaky.DrawCircleEventArgs, Leaky]])->
064c5b38(System.Object[])->
071e3c54(System.EventHandler`1[[Leaky.DrawCircleEventArgs, Leaky]])->
071e13f4(Leaky.Circle)

This Circle instance is being held by an event handler which is being tracked back to the form itself. Let’s examine why the event handler above is holding onto the Circle instance by using the dump object command.

!dumpobj 071e3c54

Name: System.EventHandler`1[[Leaky.DrawCircleEventArgs, Leaky]]
MethodTable: 00126ba8
EEClass: 6a4c3518
Size: 32(0x20) bytes
 (C:\Windows\assembly\GAC_32\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll)
Fields:
      MT    Field   Offset                 Type VT     Attr    Value Name
6a730508  40000ff        4        System.Object  0 instance 071e13f4 _target
6a72fd60  4000100        8 ...ection.MethodBase  0 instance 00000000 _methodBase
6a7331b4  4000101        c        System.IntPtr  1 instance   12c118 _methodPtr
6a7331b4  4000102       10        System.IntPtr  1 instance        0 _methodPtrAux
6a730508  400010c       14        System.Object  0 instance 00000000 _invocationList
6a7331b4  400010d       18        System.IntPtr  1 instance        0 _invocationCount

Finding the method this event handler is pointing to is a bit tricky. First, we dump the raw memory for the _methodPtr field

dd 12c118

0012c118  1c4653e9 00005f00 00126aac 00000000
0012c128  00000000 00000000 00000000 00000000
0012c138  00000000 00000000 00000000 00000000
0012c148  00000000 00000000 00000000 00000000
0012c158  00000000 00000000 00000000 00000000
0012c168  00000000 00000000 00000000 00000000
0012c178  00000000 00000000 00000000 00000000
0012c188  00000000 00000000 00000000 00000000

Then we dump the method description who’s address is located on the first line fourth column

!dumpmd 00126aac

Method Name: Leaky.Circle.Draw(System.Object, Leaky.DrawCircleEventArgs)
Class: 008f0b28
MethodTable: 00126ac4
mdToken: 06000014
Module: 00122c5c
IsJitted: yes
CodeAddr: 002f0770

The event handler is pointing to the Draw method on the Circle and keeping it in memory. I must not be unhooking the event. Looking at the source code, it becomes apparent that I am not unhooking that method anywhere in the code. The proper place to do it would be in the OnDone method of the Form1 class.

private void OnDone(object sender, EventArgs e)
{
    Circle circle = sender as Circle;
    Draw -= new EventHandler<DrawCircleEventArgs>(circle.Draw);
    AddCircle();
}

Now if I run the same test as before and dump the heap I get:

!dumpheap -stat

6a72b16c      245         5880 System.Collections.Stack
6a73303c       28         7608 System.Collections.Hashtable+bucket[]
6585394c      245        17640 System.Windows.Forms.Internal.DeviceContext
6a729b58     1193        38176 System.EventHandler
00146ba8     1230        39360 System.EventHandler`1[[Leaky.DrawCircleEventArgs, Leaky]]
6a7308ec      755        59232 System.String
00146ac4     1176        61152 Leaky.Circle
6a7040bc      315        95188 System.Object[]
0035c6b0      918      1738620      Free
6a73335c     1180     12066856 System.Byte[]

Notice there is a smaller number of circles and byte arrays and when I run for a long time the memory doesn’t go over 30 megs. There are 1176 circles but most of those are waiting to be collected. This may seem like a contrived example but from my experience it is all too real. Most memory leaks I have found in code bases are because of event handlers not being unhooked.

Tags:

Software

Powered by BlogEngine.NET 1.5.0.7
Theme by Mads Kristensen

About Me

I am a Principal Engineer with 13 years experience developing and releasing software products. I started developing in C/C++ then moved into .NET and C# and have tech lead multiple projects. I have developed products in Windows Forms, ASP.NET/MVC, Silverlight, and WPF. I currently reside in Austin, Texas.

Own Projects

Pickaxe - An easy to use web page scraper. If you know a little SQL and how basic CSS selectors work, no web scraping product will be easier to use.

Download Page

Source Code

Created ASP.NET MVC forum originally targeting home owner associations but now in use by an investor group.

http://vtss.brockreeve.com

A language for querying PGATour golf strokes.

http://pga.brockreeve.com/

Real time bidder for car ads demo

http://cargawk.com/

Simple front end tic tac toe

http://tictac.brockreeve.com/

Currently Reading