In this tutorial, we will learn how to run dotMemory and get memory snapshots. In addition, we will take a brief look at dotMemory’s user interface and basic profiling concepts. Consider this tutorial as your starting point to dotMemory.
You might ask: “What are memory snapshots and why should I get them?” This is a good time to agree on some memory profiling terms you’ll come across in this tutorial.
From memory perspective, the work of your app consists of continuous allocation of memory for new objects and releasing the memory left from the objects that are no longer used by the app. Objects are allocated one after another in the so-called managed heap (for details on memory management in .NET, follow this link). Based on this, we have two basic operations a memory profiler must be able to do:
- Get a memory snapshot. Snapshot is an instant image of the managed heap. Each snapshot contains the info about all the objects that your app has allocated in memory at the moment you clicked the Get Snapshot button.
- Collect memory traffic information. Memory traffic shows you how much memory was allocated and released in Bytes per sec. This info is also very valuable as it allows you to understand the dynamics of how your app performs.
The timeframe during which you collect traffic and get snapshots (or, in other words, profile your app) is called a profiling session.
Of course, there are some other terms that you’ll get acquainted with while following the tutorial. But for now this is enough to understand what's going on in the next couple of steps. Let’s get started!
First of all, we need an app for profiling. Through the whole series of dotMemory tutorials, we will use a single app written in C#. It emulates the classic Conway’s Game of Life that most of you probably know. (If not, please check Wikipedia. This won’t take a lot of time but will make the understanding of tutorials much easier.) So, before we start, please download and extract the archive with the app.
- Run dotMemory by using Windows Start menu.
This will open the main dotMemory window.
Now let’s start a profiling session (a timeframe during which dotMemory will collect memory usage data).
- Click the Profile button and choose Standalone application which our Game of Life app is.
This will open the Profiler Configuration window used to configure session options. And that is our next step.
* Debug builds contain compiler instructions that may affect profiling results
- In the Profiler Configuration window:
- In Application, specify the path to our Game of Life executable. It is recommended that you always profile Release builds* of your app.
- Turn on all check-boxes you see on the page. We’ll talk about them in more detail in the next tutorials.
Here is what the window should look like after you specify all the options:
- Click Run to start the profiling session. This will run our app and open the main Analysis page in dotMemory.
Once the app is running, we can get a memory snapshot. The most important thing in this operation is choosing the right moment for it. As you remember, a snapshot is the instant image of the app’s managed heap. Thus, the first thing you should do before taking a snapshot is to bring your app to the state you’re interested in. For example, if we want to take a look at the objects created right after the Game of Life app is launched, we must get a snapshot before taking any actions in the app. Conversely, if we need to know what objects are created dynamically, we must take a snapshot after we click Start in the app.
To control the profiling process, the main dotMemory page contains a number of buttons on the top of the page.
To get a memory snapshot:
- Let’s assume we need to get info about objects allocated when Game of Life runs. Therefore, click the Start button in the app and let the game run for a while.
- Click the Get Snapshot button in dotMemory.
This will capture the data and add the snapshot to the snapshot area. Getting a snapshot doesn't interrupt the profiling process, thus allowing us to get another snapshot (not needed for now).
- End the profiling session by closing the Game of Life app.
- Look at dotMemory. The main page now contains the single taken snapshot with basic information.
* If you turn off the Show unmanaged memory checkbox on top of the page, dotMemory will exclude the size of Unmanaged memory from the total value.
242.34 MB total means that the app consumes 242.34 MB of memory in total. This size is equal to Windows Task Manager’s Private Bytes---the amount of memory requested by a process. The total value consists of:
- Unmanaged memory * — memory allocated outside of the managed heap and not managed by Garbage Collector. Generally, this is the memory required by .NET CLR, dynamic libraries, graphics buffer (especially large for WPF apps that intensively use graphics), and so on. This part of memory cannot be analyzed in the profiler.
- .NET Free memory — the amount of free memory in the managed heap (not used by the app).
- .NET, used (shown as a blue bar) — the amount of memory in the managed heap that is used by the app. This is the only part of memory .NET allows you to work with. For this reason, it's also the only part which you're able to analyze in the profiler.
Let’s take a look at the snapshot in more details. To do this, click the Snapshot #1 link.
The first thing you see after opening the snapshot is the Snapshot Overview page. That page shows you hot spots in the snapshot using some fancy diagrams.
The Largest Size diagram shows types of objects that consume the major part of memory.
The Largest Retained Size diagram shows you the key objects---the ones that hold in memory all other objects in the app (more about those later in the tutorial).
The Heaps Fragmentation diagram shows the fragmentation of the managed heap segments: Generation 1, 2, and large object heap.
To ease your life, dotMemory automatically checks the snapshot for most common types of memory issues. The results of these checks are shown in the Inspections area, which consists of a number of checks: Sparse arrays, Event handlers leak, and others.
- Click the Largest Size link. This will show you all objects in the managed heap.
Now is a good time to get acquainted with the dotMemory user interface and the whole “memory analysis” stuff.
Before we go any further, let’s take a little detour and talk about how objects are stored in memory. This is needed for better understanding of what dotMemory actually shows you.
The major part of the memory consumed by your app is allocated for the app’s objects. Objects store data and reference other objects. An object and its references make up an object graph. For example, an object of the Photo class will store the id field (of the long simple type) by itself and reference other fields (objects).
|* The table of app roots is handled by the runtime.|
When your app needs memory, .NET’s Garbage Collector (GC) determines and removes the objects that are no longer needed. To do this, GC passes down the graph of each object starting with roots*, i.e. static fields, local variables and external handles. If the object is unreachable from any root, it’s considered not needed and is removed from memory. In the example below, objects D and F will be removed from memory as they cannot be accessed from the app’s roots.
Here we come to the crucial concept of retention.
A path from roots to an object may lead through a number of other objects. If all paths to object B pass through object A, then A is called a dominator for B. In other words, B is retained in memory exclusively by A. If A is garbage-collected, B will be also garbage-collected. That is why the most important parameter of each object is the size of the objects it retains. In dotMemory, this parameter is called Retained bytes. For instance, object C in the example below retains 632 bytes. Object B is not exclusively retained by C; therefore, it is not included in the calculation.
Let’s return to dotMemory and take a look at the opened Type List view. This view currently shows you all objects in the heap, sorted by the amount of memory they exclusively retain. As you can see, the major part is retained by the System.Windows.Shapes.Ellipse class (apparently, these are ellipse shapes we use to visualize Game of Life cells). Objects of that type retain 11,868,900 bytes of memory, while consuming 3,862,600 bytes by themselves.
Once you’re familiar with the main profiling terms, let’s look at how we can work with dotMemory.
We want you to think of your work in dotMemory as some sort of crime investigation (memory analysis in terms of dotMemory). The main idea here is to collect data (one or more memory snapshots) and choose a number of suspects (analysis subjects that are potentially causing the issue). So, you start with some list of suspects and gradually narrow this list down. One suspect may lead you to another and so on, until you determine the guilty one.
- Please look at the left part of the dotMemory window. It is the Analysis Path where all your investigation steps are shown.
Each item in Analysis Path is the subject you analyze. As you can see, you started with profiling GameOfLife.exe (step #1), then you opened Snapshot #1 (step #2), and at the end (step #3) you asked dotMemory to show you all objects in the heap.
As even a tiny app creates numerous objects, the attempt to analyze each object separately will not be very effective. That is why the main subject of your analysis in dotMemory is the so-called object set.
* Of course, you do not actually type any queries. All your communication with dotMemory is performed through the GUI.
Object set is a number of objects selected by a specific condition. For ease of understanding, think of an object set as of a result of some query* (very similar to an SQL query). For example, you can tell dotMemory something like "Select all objects created by SomeCall(); and promoted to Gen 2", or "Select all objects retained in memory by instance A", and so on.
- Each object set can be inspected from different perspectives called views. Look at the list that occupies the major part of the screen. It is the Type list view that shows you a plain list of objects in the set. Other views can reveal other info about the selected set. For example, the Group by Dominators view will show you who retains the selected objects in memory; the Group by Creation Stack Traces view will show you what calls created the objects; and so on.
- As mentioned above, each subject you analysis may lead you to another subject. For example, we see that the System.Windows.Shapes.Ellipse class retains most of the memory, and we want to know what objects it actually retains. To do this, open the context menu (with the right click) for the System.Windows.Shapes.Ellipse objects and select Open objects retained by this set.
This will open the object set with retained objects in the Type list view.
What you just did is asked dotMemory to “Select all objects exclusively retained by the objects of the System.Windows.Shapes.Ellipse class".
The Analysis Path now contains two more subjects:
- Ellipse. At this step, from All objects, dotMemory selected only objects of the System.Windows.Shapes.Ellipse class.
- Retained Objects. At this step, dotMemory showed all object retained by the objects of the System.Windows.Shapes.Ellipse class.
In this fashion, by following from one analysis subject to another and inspecting them in different views, you move to the cause of your memory issues.
- Experiment with dotMemory a little bit. For example, determine the call that originates objects of the GameOfLife.Cell class.