Memory profiling can hardly be called "utilities for daily use." Most often, developers are thinking about profiling their product before the release itself. Such an approach may well work, but only as long as any memory problem that was detected at the last moment (for example, a memory leak or large memory traffic) does not destroy all your plans. One solution could be profiling on a regular basis, but hardly anyone would want to spend such valuable time on it. However, the solution seems to be there.
If unit testing is an integral part of your development process, then you regularly run numerous tests that test the functionality of the application. Now imagine that you can write some special "tests for memory use." For example, a test that detects a leak by checking the memory for the presence of objects of a certain type, or a test that monitors memory traffic and “drops” if the traffic (alloted volume) exceeds a specified threshold. This is exactly what the
dotMemory Unit framework allows you to do. The dotMemory Unit is distributed as a NuGet package and allows you to run the following scripts:
- Check memory for objects of a certain type.
- Check memory traffic.
- Comparison of snapshots (hereinafter 'snapshots') of memory.
- Saving snapshots to disk for later analysis in dotMemory (JetBrains memory profiler).
In other words, the dotMemory Unit extends the capabilities of your unit-testing framework with the functionality of a memory profiler.
How it works?
Example 1: Checking memory for certain objects
Let's start with something simple. One of the most useful scenarios is leak detection by checking memory for objects of a certain type.
[Test] public void TestMethod1() { ...
- Lambda is passed to the
Check
method of the static dotMemory
class. This method will only be called if you run this test using the Run Unit Tests menu under the dot Unit Unit . - The
memory
object passed to the lambda contains data about all objects in memory at the current program execution point. - The
GetObjects
method returns a set of objects corresponding to the condition passed in the next lambda. For example, this line of code selects only objects of type Foo
from memory. The Assert
expression assumes that there must be 0
objects of type Foo
in memory.
Please note that the dotMemory Unit does not oblige you to use any specific syntax for Assert
. Just use the syntax of the framework for which your test was written. For example, the line from the example above (written for NUnit) can be rewritten for MSTest:
Assert.AreEqual(0, memory.GetObjects(where => where.Type.Is<Foo>()).ObjectsCount);
dotMemory Unit allows you to select objects for almost any condition, get data on the number of objects and use them in
Assert
expressions. For example, you can make sure that the Large object heap contains no objects:
Assert.That(memory.GetObjects(where => where.Generation.Is(Generation.Loh)).ObjectsCount, Is.EqualTo(0));
')
Example 2: Check Memory Traffic
The test for checking memory traffic (allocated data volume) looks even easier. All that is required of you is to “tag” the test with the
AssertTraffic
attribute. In the following example, we assume that the memory allocated by
TestMethod1
does not exceed 1000 bytes.
[AssertTraffic(AllocatedMemoryAmount = 1000)] [Test] public void TestMethod1() { ...
Example 3: Complicated scripts for checking memory traffic
If you need more detailed information about traffic (for example, data about allocations of objects of a particular type), you can use an approach similar to that shown in Example 1. The lambdas passed to the
dotMemory.Check
method
dotMemory.Check
you to filter data according to various conditions.
var memoryCheckPoint1 = dotMemory.Check(); // 1 foo.Bar(); var memoryCheckPoint2 = dotMemory.Check(memory => { // 2 Assert.That(memory.GetTrafficFrom(memoryCheckPoint1).Where(obj => obj.Interface.Is<IFoo>()).AllocatedMemory.SizeInBytes, Is.LessThan(1000)); }); bar.Foo(); dotMemory.Check(memory => { // 3 Assert.That(memory.GetTrafficFrom(memoryCheckPoint2).Where(obj => obj.Type.Is<Bar>()).AllocatedMemory.ObjectsCount, Is.LessThan(10)); });
- In order to mark the time interval on which you want to analyze traffic, use the “checkpoint” created by the same
dotMemory.Check
method (as you may have guessed, this method simply takes a snapshot of the memory at the time of the call). - The checkpoint defining the starting point of the interval is passed to the
GetTrafficFrom
method.
For example, this line assumes that the total size of objects implementing the interface IFoo
and created between memoryCheckPoint1
and memoryCheckPoint2
does not exceed 1000 bytes.
- You can get data on any of the previously created checkpoints. So, this line requests traffic data between the current
dotMemory.Check
call and memoryCheckPoint2
.
Example 4: Comparing Snapshots
Just as in the adult profile dotMemory, you can use checkpoints not only to analyze traffic, but also to compare them with each other. In the example below, we assume that none of the objects belonging to the
MyApp
namespace survive the garbage collection in the interval between
memoryCheckPoint1
and the second call to
dotMemory.Check
.
var memoryCheckPoint1 = dotMemory.Check(); foo.Bar(); dotMemory.Check(memory => { Assert.That(memory.GetDifference(memoryCheckPoint1) .GetSurvivedObjects().GetObjects(where => where.Namespace.Like("MyApp")).ObjectsCount, Is.EqualTo(0)); });
Conclusion
The dotMemory Unit is very flexible and allows you to completely control the memory usage of your application. Use the "memory tests" as you use the normal tests:
- After you discover a memory leak yourself, write a test that covers this part of the code.
- Write integration tests using the dotMemory Unit to ensure that the new “features” do not create memory problems.