Automatic memory leak testing

Use this forum for questions on how to use .NET Memory Profiler and how to analyse memory usage.
Post Reply
Dante
Posts: 3
Joined: Thu Mar 08, 2007 3:46 pm

Automatic memory leak testing

Post by Dante » Fri Mar 09, 2007 11:06 am

Hi,

after spending several year memory profiling using .NET Memory Profiler we want to automate this process. I have started to use your API but I have some questions:

1. What is the difference of NoNewInstances and NoNewInstancesExcept?
2. Can I track COM memory leak as well?
3. What are known memory leaks in .NET 2.0?


1. I think I understood the difference but an example would be helpful and when should I use the Except method.

2. Out application is written in C# which is massivly using underlying COM objects. So the target is to track also leaking COM object. If I try to profile it I don't get the expected results as I am would see it in the .NET MP application, here some example code:

Code: Select all

private MemSnapShot lastSnap = MemSnapShot.Empty;
private void MemTest()
{
    MyComObject myObject = null;
    if( lastSnap != MemSnapShot.Empty ) 
        myObject = new MyComObjectClass();

    MemSnapShot snap = MemProfiler.FullSnapShot();
    try
    {
        MemAssertion.BeginAssertions();

        if( lastSnap != MemSnapShot.Empty )
            Debug.WriteLine( "NoNewInstances: " + MemAssertion.NoNewInstances( lastSnap, typeof( MyComObject ) ) );
    }
    catch( Exception ex )
    {
        // logged exception
    }
    finally
    {
        lastSnap = snap;
        MemAssertion.EndAssertions();
    }
}
The idea behind the code is to create a starting snapshot and when finished with the thing I want to profile to take another snapshot and compare it with the previous to test if a leaking item exists.
This code works for .NET classes and also for COM AxControl wrappers but if I use one of our COM objects the NoNewInstances method returns true!? Even if I create the item before making the second snapshot (see code). But then again the .NET MP will show an increase by one for the specified object (so it was created) therefore I wonder what I have to do to make it work? It makes no difference to check for MyComObject or MyComObjectClass.

3. I guess that Microsoft has changed in .NET 2.0 some memory cleanups to prevent memory leaks. E.g. if I created in .NET 1.1 a dialog in which I subscribed to events then it was necesarry to unsubscribe from the events again before disposing the dialog or otherwise this was creating a memory leak. This seems to be fixed in .NET 2.0. because I can not reproduce it anymore with the .NET MP(?). Is this true or a bug in your application?
If it is true do you know which potential memory leaks were fixed as well?
Also the question would be what I have to make wrong to create a memory leak? Just to know what I have to look for and to test the result.

Hope that you have some answers for me

Ciao
Dante

I am using :
.NET MP v2.6.97.0
C# for the application with .NET 2.0
C++ for COM objects created with VS2003

Andreas Suurkuusk
Posts: 1029
Joined: Wed Mar 02, 2005 7:53 pm

Post by Andreas Suurkuusk » Fri Mar 09, 2007 3:14 pm

Hi,
  1. The NoNewInstances methods are used to make sure that no new instances exists of one or more specific classes. For instance if you create a Windows form and then dispose it, you know that no new controls should be left in memory. So you can use the following assertion:

    Code: Select all

    MemAssertion.NoNewInstances( typeof( Control ), true );
    The downside of using this type of assertion is that you could miss memory leaks from other classes. For instance, maybe you have loaded a large document, which for some reason doesn't get GCed. This will not be noted by the above assertion.

    On the other hand you, if you use the NoNewInstancesExcept methods, you will be able to check all classes in the application for memory leaks, except for a few excluded ones. When asserting on all classes, there is a risk that some instances become falsely identified as memory leaks. This can be avoided by excluding the classes of those instances from the assertion.

    NOTE! Calling the NoNewInstances methods that don't accept any Type(s) is identical to calling the NoNewInstancesExcept with an empty Type array.
  2. The profiler does not track the actual unmanaged COM instances, but you can use the profiler to track the COM Callable Wrappers (CCW) and the Runtime Callable Wrappers (RCW). The assertion you performed in your example was made on the MyComObject interface and not on the MyComObjectClass class. Currently the profiler does not support interface assertions, so you must make sure that the provided type is a class. If you change MyComObject to MyComObjectClass I think your example will work.

    NOTE! The runtime doesn't always know the managed type of the RCW. It might not even exist. In such cases the RCW will be of the type System.__ComObject. You might want to assert on that class as well.
  3. In .NET 1.1 there were several memory leaks in the framework. As far as I know most of them have been fixed in .NET 2.0. One memory leak that I know of in .NET 2.0 is related to the ToolStrip control. There is a problem if the ToolStrip contains a text box and the ToolStrip itself has dynamic visibility. Other than that I cannot currently remember any known memory leaks.

    NOTE! Normally you should not need to remove event handlers that are added to a form or any of its child controls. You only need to remove event handlers when they're added to an external object that might have a longer lifetime than the form. In case you have a control that might not be correctly GCed, e.g. the NumericUpDown in .NET 1.x, you can minimize the memory problem by explicitly removing the handlers.

    Even though you don't need to remove all added event handlers, I think the biggest memory error that .NET programmers create, is forgetting to remove an event handler.
Best regards,

Andreas Suurkuusk
SciTech Software AB

Dante
Posts: 3
Joined: Thu Mar 08, 2007 3:46 pm

Post by Dante » Fri Mar 09, 2007 5:11 pm

Thanx for the quick answer.

I tried to replace the COM Interface with the class name but it is still not returning the expected results. I create a COM object just before creating a snapshot

Code: Select all

MyComObjectClass myObject = new MyComObjectClass();

MemSnapShot snap = MemProfiler.FullSnapShot();

Debug.WriteLine( "1:" + MemAssertion.NoInstances( typeof( MyComObjectClass ) ) );
Debug.WriteLine( "2:" + MemAssertion.NoNewInstances( lastSnap, typeof( MyComObjectClass) ) );
Debug.WriteLine( "3:" + MemAssertion.NoNewInstancesExcept( lastSnap, typeof( MyComObjectClass) ) );
The results are:
1.True
2.True
3.False

If NoNewInstancesExcept() would be the way to go it would be the expected result. But doing the snapshot again without creating the COM object leads to the same results but I would expect 3. being True. Is there somethings in the options to change to make it work?

BTW: We moved to Version 3.0.89.0

Ciao
Dante

Andreas Suurkuusk
Posts: 1029
Joined: Wed Mar 02, 2005 7:53 pm

Post by Andreas Suurkuusk » Sun Mar 11, 2007 8:29 pm

Hi,

The code in your second post didn't include a reference to lastSnap, but I assume that your code should look something like:

Code: Select all

private MemSnapShot lastSnap = MemSnapShot.Empty;
private void MemTest()
{
	MyComObject myObject = new MyComObjectClass();

	MemSnapShot snap = MemProfiler.FullSnapShot();

	Debug.WriteLine( "1:" + MemAssertion.NoInstances( typeof( MyComObjectClass ) ) );
	Debug.WriteLine( "2:" + MemAssertion.NoNewInstances( lastSnap, typeof( MyComObjectClass ) ) );
	Debug.WriteLine( "3:" + MemAssertion.NoNewInstancesExcept( lastSnap, typeof( MyComObjectClass ) ) ); 

	lastSnap = snap;
}
In this case you create a new instance of MyComObjectClass and assign it to a local variable. Then you perform a full snapshot, which in turn will perform a full GC. Since you are no longer using the local variable "myObject", you have no references to the recently created MyComObjectClass (at least when you run on a Release build). Thus the MyComObjectClass will be GCed by the FullSnapshot.

The NoInstances assertion returns true since all MyComObjectClass instances have been GCed.

The NoNewInstances assertion also returns true since all MyComObjectClass instances have been GCed, including any new ones.

The NoNewInstancesExcept returns false, since instances other than MyComObjectClass might have been created before the assertion runs. Note that the assertion methods themselves create instances, such as string instances. To avoid side effects, you should always use BeginAssertions/EndAssertions when using the NoNewInstancesExcept assertions.
Best regards,

Andreas Suurkuusk
SciTech Software AB

Dante
Posts: 3
Joined: Thu Mar 08, 2007 3:46 pm

Still not the results as expected...

Post by Dante » Mon Mar 12, 2007 2:57 pm

Hi Andreas,

thx again for your answer but after making some changes to the sample code it is still not working as I would expect. To make it a bit clearer I created a very simple sample project. As you will see in the code below I am storing created COM and .NET objects in a list which is a member of the class. Therefore these objects should not be destroyed by the CRL. I also "use" the object by calling the ToString() method If I test this with a COM object I get always the described false behaviour that NoInstance is available and NoNewInstance was created - neither the interface nor the class. But the profiling app shows a different result which I would like to have.
Doing the same thing with a simple .NET class works like a charm. In both cases it returns "False"!

With the provided code it should be easy to reproduce what I am doing and I hope that you could find the bug.


1. Create a project with VS2005


2. Add references
- NET Tab: .NET Memory Profiler API (Version 3.0.89.0, Runtime v2.0.50727)
- COM Tab: Microsoft ADO Ext. 2.8 for DDL and Security (Version 2.8) - I think this should be available on your system if you have it up-to-date


3. Create a form with 4 buttons and a big label for the output
- Button 1 is for creating the COM snap ("Create COM")
- Button 2 is for clearing the COM objects ("Clear COM")
- Button 3 is for creating a snap with a created .NET class ("Create NET")
- Button 4 is for clearing the .NET classes ("Clear NET")
->Double click on the buttons to create event handlers for them


4. Add to the using block

Code: Select all

using ADOX; // reference to Microsoft ADO Ext. 2.8 for DLL and Security - used for creating the CatalogClass COM object
using SciTech.NetMemProfiler;

5. Create a .NET class

Code: Select all

    public class MyNetClass
    {
        public MyNetClass()
        { 
        }
    }

6. Add member variables

Code: Select all

        private int snapId = 0;
        private MemSnapShot lastSnap = MemSnapShot.Empty;
        private List<CatalogClass> comList = new List<CatalogClass>();
        private List<MyNetClass> netList = new List<MyNetClass>();
7. These are the event handlers for the four buttons

Code: Select all

        private void button1_Click( object sender, EventArgs e )
        {
            // create COM object for the second snap
            if( lastSnap != MemSnapShot.Empty && comList.Count == 0 )
                comList.Add( new ADOX.CatalogClass() );

            StringBuilder builder = new StringBuilder();
            MemSnapShot snap = MemProfiler.FullSnapShot( "Snap COM " + (++snapId) );

            try
            {
                MemAssertion.BeginAssertions();

                builder.AppendLine( "COM Snap at " + DateTime.Now.ToString() );

                builder.AppendLine( "COM NoInstance: " + MemAssertion.NoInstances( typeof( CatalogClass ) ) );
                builder.AppendLine( "" );

                if( lastSnap != MemSnapShot.Empty )
                {
                    builder.AppendLine( "COM NoNewInstances (Interface): " + MemAssertion.NoNewInstances( lastSnap, typeof( Catalog ) ) );
                    builder.AppendLine( "COM NoNewInstances (Class): " + MemAssertion.NoNewInstances( lastSnap, typeof( CatalogClass ) ) );
                    builder.AppendLine( "" );
                    // uncommented to ignore exceptions
                    //builder.AppendLine( "NoNewInstancesExcept (Interface): " + MemAssertion.NoNewInstancesExcept( lastSnap, typeof( Catalog ) ) );
                    //builder.AppendLine( "NoNewInstancesExcept (Class); " + MemAssertion.NoNewInstancesExcept( lastSnap, typeof( CatalogClass ) ) );
                }
                else
                    builder.AppendLine( "COM NoNewInstances (snap): empty" );

                // just for savety to ensure that the 
                if( comList.Count > 0 )
                    comList[ 0 ].ToString();

                builder.AppendLine( "" );
            }
            catch( Exception ex )
            {
                System.Diagnostics.Debug.WriteLine( ex.Message );
            }
            finally
            {
                lastSnap = snap;
                MemAssertion.EndAssertions();

                label1.Text = builder.ToString();
            }
        }

        private void button2_Click( object sender, EventArgs e )
        {
            // release COM object for the second snap
            comList.Clear();

            StringBuilder builder = new StringBuilder();
            MemSnapShot snap = MemProfiler.FullSnapShot( "Snap COM " + ( ++snapId ) );
            try
            {
                MemAssertion.BeginAssertions();

                builder.AppendLine( "COM Snap at " + DateTime.Now.ToString() );

                builder.AppendLine( "COM NoInstance: " + MemAssertion.NoInstances( typeof( CatalogClass ) ) );
                builder.AppendLine( "" );

                if( lastSnap != MemSnapShot.Empty )
                {
                    builder.AppendLine( "COM NoNewInstances (Interface): " + MemAssertion.NoNewInstances( lastSnap, typeof( Catalog ) ) );
                    builder.AppendLine( "COM NoNewInstances (Class): " + MemAssertion.NoNewInstances( lastSnap, typeof( CatalogClass ) ) );
                    builder.AppendLine( "" );
                    //builder.AppendLine( "COM NoNewInstancesExcept (Interface): " + MemAssertion.NoNewInstancesExcept( lastSnap, typeof( Catalog ) ) );
                    //builder.AppendLine( "COM NoNewInstancesExcept (Class); " + MemAssertion.NoNewInstancesExcept( lastSnap, typeof( CatalogClass ) ) );
                }
                else
                    builder.AppendLine( "COM NoNewInstances (snap): empty" );

                builder.AppendLine( "" );
            }
            catch( Exception ex )
            {
                System.Diagnostics.Debug.WriteLine( ex.Message );
            }
            finally
            {
                lastSnap = snap;
                MemAssertion.EndAssertions();

                label1.Text = builder.ToString();
            }
        }

        private void button3_Click( object sender, EventArgs e )
        {
            // create NET object for the second snap
            if( lastSnap != MemSnapShot.Empty && netList.Count == 0 )
                netList.Add( new MyNetClass() );

            StringBuilder builder = new StringBuilder();
            MemSnapShot snap = MemProfiler.FullSnapShot( "Snap NET " + ( ++snapId ) );

            try
            {
                MemAssertion.BeginAssertions();

                builder.AppendLine( "NET Snap at " + DateTime.Now.ToString() );

                builder.AppendLine( "NET NoInstance: " + MemAssertion.NoInstances( typeof( MyNetClass ) ) );
                builder.AppendLine( "" );

                if( lastSnap != MemSnapShot.Empty )
                {
                    builder.AppendLine( "NET NoNewInstances (Class): " + MemAssertion.NoNewInstances( lastSnap, typeof( MyNetClass ) ) );
                    builder.AppendLine( "" );
                    // uncommented to ignore exceptions
                    //builder.AppendLine( "NoNewInstancesExcept (Class); " + MemAssertion.NoNewInstancesExcept( lastSnap, typeof( MyNetClass ) ) );
                }
                else
                    builder.AppendLine( "NET NoNewInstances (snap): empty" );

                // just for savety to ensure that the 
                if( netList.Count > 0 )
                    netList[ 0 ].ToString();

                builder.AppendLine( "" );
            }
            catch( Exception ex )
            {
                System.Diagnostics.Debug.WriteLine( ex.Message );
            }
            finally
            {
                lastSnap = snap;
                MemAssertion.EndAssertions();

                label1.Text = builder.ToString();
            }
        }

        private void button4_Click( object sender, EventArgs e )
        {
            // release NET object for the second snap
            netList.Clear();

            StringBuilder builder = new StringBuilder();
            MemSnapShot snap = MemProfiler.FullSnapShot( "Snap NET " + ( ++snapId ) );
            try
            {
                MemAssertion.BeginAssertions();

                builder.AppendLine( "NET Snap at " + DateTime.Now.ToString() );

                builder.AppendLine( "NET NoInstance: " + MemAssertion.NoInstances( typeof( MyNetClass ) ) );
                builder.AppendLine( "" );

                if( lastSnap != MemSnapShot.Empty )
                {
                    builder.AppendLine( "NET NoNewInstances (Class): " + MemAssertion.NoNewInstances( lastSnap, typeof( MyNetClass ) ) );
                    builder.AppendLine( "" );
                    //builder.AppendLine( "NET NoNewInstancesExcept (Class); " + MemAssertion.NoNewInstancesExcept( lastSnap, typeof( MyNetClass ) ) );
                }
                else
                    builder.AppendLine( "NET NoNewInstances (snap): empty" );

                builder.AppendLine( "" );
            }
            catch( Exception ex )
            {
                System.Diagnostics.Debug.WriteLine( ex.Message );
            }
            finally
            {
                lastSnap = snap;
                MemAssertion.EndAssertions();

                label1.Text = builder.ToString();
            }
        }
    }

8. The idea with the buttons:
COM part:
Click Button1: Creates initial snapshot
Click Button1 again: Creates COM object and a second snapshot - compares with previous snap for COM object and returns "True" results (the thing I don't understand)
Click Button2: Clears COM objects and creates another snap. Primarly it is there to reproduce step 2.

.NET part:
Click Button3: Creates .NET object and compares snap with previous one. In all cases it returns "False" which is what I would expect.
Click Button4: Clears .NET objects and returns "True" which I am also expecting.


IMHO the API works perfect with .NET objects but I still don't get it to work with the COM part. If this would work it would makes memory testing so much easier and a lot of people would benefit from it due to the fact that there is no .NET build in mechanism for checking created instances. If you like I can send you the sample project.


Ciao
Dante

Andreas Suurkuusk
Posts: 1029
Joined: Wed Mar 02, 2005 7:53 pm

Post by Andreas Suurkuusk » Mon Mar 12, 2007 4:03 pm

Thank you for the repro. Now I have been able to reproduce your problem and verify that there is a bug.

The API identifies the Type using the full type name. The type name of the COM-object refers to the interop dll (in your example Interop.ADOX.dll). However, when the profiler retrieves information about the type, it associates the type with the unmanaged dll implementing the COM object (in your example ADOX.dll). This causes the profiler to fail to identify the correct type and the assertion will not work correctly.

We will most likely fix this problem in the next maintenance release, but in the meantime there is a workaround you can use. Instead of providing the Type to the assertion methods, you can provide the name of the type.

E.g. MemAssertion.NoInstances( "ADOX.CatalogClass" );

This will avoid the problem with mismatched dll-names and should work correctly as long as you don't have different dlls implementing COM-classes with the same names.
Best regards,

Andreas Suurkuusk
SciTech Software AB

Post Reply

Who is online

Users browsing this forum: No registered users and 13 guests