Use Assertions to Detect Memory Leaks
Another part of the API are the assertions, which can be used to detect potential memory leaks.
In many cases, it is possible to know how an operation will affect the memory usage of the managed heap when it is executed. For instance, some operations should not create new live instances of certain classes after it has finished. By using the assertion methods in the MemAssertion class you can make sure that the memory usage of the operation behaves as expected. There are two ways of performing memory assertions:
Defining the assertions in an AssertionsDefinition instance and then perform the assertions using the
MemAssertion.Assert
method.Using the NoInstances, NoNewInstances and NoNewInstancesExcept methods in the MemAssertion class.
The AssertionsDefinitions approach requires slightly more coding, but it is much more flexible and provides additional memory checks, so it is the recommended approach when performing memory assertions.
To perform a memory assertion, you have to perform the following:
Establish a base snapshot using the method
MemProfiler.FastSnapshot()
.
The base snapshot is used as a reference when looking for new instances.Perform the operation that you want to check for memory leaks.
Analyze the memory impact of the operation and write memory assertions that checks that instances are garbage collected as expected.
To catch as many memory leaks as possible, it is desirable to include memory assertions that check for as many instances as possible. The problem is that many operations may have side effects on memory that are not easy to anticipate. For instance, when performing an operation for the first time, new Type
s might get loaded, creating unexpected new instances of RuntimeType
s and String
s. It is therefore important to remember that the instances that are identified as memory leaks are identified only as potential memory leaks. The potential memory leak instances have to be analyzed to decide whether they are real leaks or if they are identified falsely as memory leaks by an incorrect memory assertion.
The risk of falsely identifying instances as leaks is bigger when using the MemAssertion.NoNewInstancesExcept methods (or the MemAssertion.NoNewInstances and AssertionsDefinition.NoNewInstances methods without the Type
argument), since these methods include all loaded types in the process, except the ones that have been specifically selected for exclusion. If these methods are used, do not perform the assertions the first time the operation is performed, so that side effects, e.g., from loading types, can be avoided.
Example:
Consider the ShowDialog
method below, which shows a modal dialog (derived from System.Windows.Forms.Form
):
void ShowDialog()
{
using( SomeDialog dlg = new SomeDialog() )
{
dlg.ShowDialog();
}
}
Creating a dialog, showing it, and then disposing it, are operations that should not create any new live instances. In reality, however, quite a few new live instances might be created as a side effect of creating the dialog. The first time the method is executed the SomeDialog
type might get loaded, creating a new live Type
instance (actually a RuntimeType
), and new strings (e.g., the name of the type).
In this case, it is known that after the dialog has been shown and disposed, it should be eligible for garbage collection. If the SomeDialog
instance cannot be garbage collected, then we might have something referencing the instance unintentionally (for instance, a left-over event handler).
To assert that the SomeDialog
instance can be garbage collected, the NoNewInstances
method can be used:
MemAssertion.NoNewInstances( typeof( SomeDialog ) );
This method will make a full garbage collect and then assert that no new instances of the SomeDialog
class exist on the GC heap.
The ShowDialog code with the memory assertion looks like this:
using SciTech.NetMemProfiler;
void ShowDialog()
{
// Establish a base snapshot
MemProfiler.FastSnapshot();
using( SomeDialog dlg = new SomeDialog() )
{
dlg.ShowDialog();
}
// Assert that no new instances of SomeDialog has been
// created. The FastSnapshot collected at the
// beginning of the method will be used as
// reference.
MemAssertion.NoNewInstances( typeof( SomeDialog ) );
}
If the memory assertion fails, i.e., a new instance of SomeDialog
does exist, a full snapshot will be collected and reported to the profiler, and the instance of SomeDialog
will be marked as a potential memory leak.
One problem with the example above is that it only detects instances of SomeDialog
as potential memory leaks. The SomeDialog
class probably contains a set of child Control
s, and all of them should also be eligible for garbage collection after the dialog has been disposed. All those instances can also be checked by changing the NoNewInstances
assertion to:
MemAssertion.NoNewInstances( typeof( Control ), true );
The second argument to NoNewInstances
is a Boolean
value indicating whether subclasses of the specified Type
should be checked as well.
Note
When profiling a debug build of a program, the scope of the local variables is often longer than it might appear. In the example above, the dlg
variable might still be in use at the time of the assertion. This will prevent the SomeDialog
instance from being collected, and the assertion will fail, falsely identifying the SomeDialog
instance as a memory leak.
To avoid this, use a release build or wrap the tested code in a method:
using SciTech.NetMemProfiler;
void DoShowDialog()
{
using( SomeDialog dlg = new SomeDialog() )
{
dlg.ShowDialog();
}
}
public void ShowDialog()
{
// Establish a base snapshot
MemProfiler.FastSnapshot();
DoShowDialog();
// Assert that no new instances of SomeDialog has been
// created. The FastSnapshot collected at the
// beginning of the method will be used as
// reference.
MemAssertion.NoNewInstances( typeof( SomeDialog ) );
}