© The Author(s), under exclusive license to APress Media, LLC, part of Springer Nature 2023
V. SarcarSimple and Efficient Programming with C# https://doi.org/10.1007/978-1-4842-8737-8_13

13. Analyzing Memory Leaks

Vaskaran Sarcar1  
(1)
Kolkata, West Bengal, India
 

Before we discuss memory leaks, let’s analyze a simple case study. It will help you understand how memory leaks can hamper the smooth execution of an application over the long run. Suppose you have an online application where users need to fill in some data and then click the submit button. We can assume that during this process, you need to use some resources that consume some blocks of computer memory. Obviously, once the process is completed, developers release the allocated blocks of memory. Now assume that unfortunately the developers of the application mistakenly forgot to deallocate some blocks during the process of execution. Because of this misjudgment, let’s say that the application leaks 512 bytes per click. You probably won’t notice any performance degradation from some initial clicks. But what happens if thousands of online users use the application simultaneously? If 1,000,000 users click the submit button, the application will eventually cause 488 MB of memory loss. You cannot allow this for a long time, because if it goes like this, the application becomes slower and slower, and in the worst case, the application can crash. These are common symptoms caused by memory leaks.

This is why in the previous chapter I told you that if a computer program runs over a long time but fails to release memory resources, you may face some challenging situations like the following:
  • A machine becomes slow over time.

  • A specific operation in an application takes longer to execute.

  • The worst case is that an application/system can crash.

In short, even if a program leaks a small amount of data for a common operation, it is quite obvious that you will see some kind of malfunction over time; for example, your device may crash with a System.OutOfMemoryException, or operations in the device may become so slow that you need to restart the application often. It is apparent that “how fast it comes to your attentiondepends on the leaking rate of the application.

Managed vs. Unmanaged Memory Leaks

I hope that you read Chapter 12 carefully. There you learned that if you work with C++, you are responsible for the memory deallocations. But in the case of C#, the CLR plays a big role, and most of the time it helps you not to worry about the deallocations.

Still, there are situations when you need to take the responsibility for deallocations. For example, in the case of handling events, you need to be careful: if you register an event, you need to unregister it. This book is about C#, so we’ll discuss managed memory leaks, and to illustrate this, I’ll use events.

I assume that you know how to use events in an application. A detailed discussion of events and delegates is beyond the scope of this book. However, I have added some supportive comments in the code to help you understand it a little better. I acknowledge that it may be easy for you to find the problem in this program. But my core intention is to show you how to analyze the event leaks using the diagnostic tools in Visual Studio. The Diagnostic Tools window can help analyze CPU and memory usage. Here you can view events that show performance-related information.

Note

I discuss delegates, events, and other topics in detail in my other books, Interactive C# and Getting Started with Advanced C#, also published by Apress. The first book shows the usage of the diagnostic tools as well as Microsoft’s CLR Profiler to analyze memory leaks. The second one discusses delegates and events in depth. Also, my recent book Test Your Skills in C# Programming covers these topics. So, if you are interested, you can take a look at those books.

Before I show you how to use the diagnostic tool, I want you to recall that you can programmatically find the current memory consumption of a program’s objects using the GetTotalMemory method. I used this method in demonstration 1 of Chapter 12 as follows:
Console.WriteLine($"Total Memory allocation:
  {GC.GetTotalMemory(false)}");
You may wonder why I did not pass the true argument, because it tells the GC to perform a collection first. The answer is that I found that introducing the sleep time in that program helped me to get a more consistent result rather than purely relying on this true parameter. Let me pick the parameter description from this method description for your immediate reference. The following description also does not guarantee that the collection will be completed before you see the result.
// Parameters:
//   forceFullCollection:
//     true to indicate that this method can wait for
//     garbage collection to occur before
//     returning; otherwise, false.

So, since I could not rely on the true parameter, I made it false in that program.

Memory Leak Analysis

Now the question is, how can you detect leaks? Let me tell you that finding a memory leak in an application is a challenging task! There are many tools for this purpose. For example, windbg.exe is a common tool to find memory leaks in a large application. In addition, you can use other graphical tools, such as Microsoft’s CLR Profiler, SciTech’s Memory Profiler, Red Gate’s ANTS Memory Profiler, and so forth to find the leaks in your system. Many organizations have their company-specific memory leak tools to detect and analyze leaks. In my previous organization, our experts developed such a tool. It is a wonderful tool. I was fortunate because I used it for several years and learned many interesting things about catching memory leaks.

Visual Studio users can use a variety of profiling tools for diagnosing different kinds of app performance issues depending on the app type. In the latest editions of Visual Studio, you can see a Diagnostic Tools window. It shows you the profiling tools that are available during a debugging session. You can use it to detect and analyze memory leaks. It is very user-friendly and easy to use. Using this tool, you can take various memory snapshots. Markers in the tool indicate garbage collector activities. These are very useful and effective: you can analyze the data in real time while the debugging session is active. The spikes in the graph can draw your attention immediately. The following program shows you a sample demonstration.

Before you run this application, ensure that you enable the option to launch the Diagnostic Tools, as shown in Figure 13-1. In the Visual Studio IDE, you can see this option in Tools ➤ Option ➤ Debugging ➤ General.
Figure 13-1

Enabling the “Diagnostic Tools while debugging” option in Visual Studio Community 2022

If you do not see this option on your machine, you can launch the Visual Studio Installer, click the Modify button, go to the Individual Component tab, and then check whether you have installed the profiling tools on your machine. Figure 13-2 shows you an example.
Figure 13-2

Checking whether .NET profiling tools are installed in the machine

Demonstration

Here is the complete demonstration. Inside the client code, you’ll see two methods: one to register the events and one to unregister the event. You can see that by mistake, I have registered too many events, but I have unregistered only one of them. And these remaining events will cause leaks in this application.
Console.WriteLine("***Creating custom events and
  analyzing memory leaks.***");
Sender sender = new();
Receiver receiver = new();
Helper.RegisterNotifications(sender, receiver);
Helper.UnRegisterNotification(sender, receiver);
delegate void IdChangedHandler(object sender,
  IdChangedEventArgs eventArgs);
class IdChangedEventArgs : EventArgs
{
    public int IdNumber { get; set; }
}
class Sender
{
    public event IdChangedHandler? IdChanged;
    private int Id;
    public int ID
    {
        get
        {
            return Id;
        }
        set
        {
            Id = value;
            // Raise the event
            OnMyIntChanged(Id);
        }
    }
    protected void OnMyIntChanged(int id)
    {
      if (IdChanged != null)
       {
         // It is the simplified form of the
         // following lines:
         // IdChangedEventArgs idChangedEventArgs =
         //  new IdChangedEventArgs();
         // idChangedEventArgs.IdNumber = id;
         IdChangedEventArgs idChangedEventArgs = new()
          {
              IdNumber = id
          };
         //if (IdChanged != null)
         //{
         //    IdChanged(this, idChangedEventArgs);
         //}
         //Simplified form
         IdChanged?.Invoke(this, idChangedEventArgs);
     }
  }
}
class Receiver
{
    public void GetNotification(object sender,
     IdChangedEventArgs e)
    {
        Console.WriteLine($"Sender changed the id
          to:{e.IdNumber}");
    }
}
class Helper
{
    public static void RegisterNotifications(Sender
     sender, Receiver receiver)
    {
        for (int count = 0; count < 10000; count++)
        {
            // Registering too many events.
            sender.IdChanged += receiver.GetNotification;
            sender.ID = count;
        }
    }
    public static void UnRegisterNotification(Sender
      sender, Receiver receiver)
    {
        // Unregistering only one event.
        sender.IdChanged -= receiver.GetNotification;
    }
}
I ran this program and start taking different snapshots when the program is running. (If you do not see the Diagnostic Tools window, you can bring up the window from Debug ➤ Windows ➤ Show Diagnostic Tools or press Ctrl+Alt+F2). Figure 13-3 shows the Diagnostic Tools window; it includes five different snapshots to analyze memory usage at a given point in time.
Figure 13-3

Different snapshots are taken using the Diagnostic Tools in Visual Studio

Snapshots from Diagnostic Tools

Let’s analyze the difference (shown in the column Objects (Diff)). For example, let me pick the row that is associated with ID 4. This row shows the objects count increased by 72 compared to the previous snapshot. If you hover your mouse on this, it will indicate that you can open the heap diff view for the selected snapshot sorted by object count. Let’s click this link. See Figure 13-4.
Figure 13-4

The object count difference in a particular snapshot

We can see how the heap size is growing over time. See that IdChangedEventHandler is causing the damage. Now you can recognize from the code that by mistake, I am registering an event repeatedly inside the for loop in this code:
sender.IdChanged += receiver.GetNotification;
Actually, I have placed the for loop in the wrong place. I needed to remove it from the RegisterNotifications method. Instead, I needed to place this loop inside the client code as follows:
Console.WriteLine("***Creating custom events and
  analyzing memory leaks.***");
Sender sender = new();
Receiver receiver = new();
for (int count = 0; count < 10000; count++)
{
    Helper.RegisterNotifications(sender, receiver);
    sender.ID = count;
    Helper.UnRegisterNotification(sender, receiver);
}
// The remaining code is skipped

Similarly, I could show you the leak using Microsoft’s CLR profiler. But showing the usage of different tools is not the aim of the chapter. Instead, you want to prevent memory leaks using any tool you like. Since the diagnostic tool is already available in the latest editions of Visual Studio, I do not want to miss the opportunity to show its usage.

I want to tell you that catching a memory leak needs expertise because it is not very easy. In the previous demonstration, our program has a few methods; that is why it is easy to catch the leak. But think about some typical scenarios:
  • You use third-party code that has a leak. However, you cannot immediately find it, because you do not have access to the code.

  • Let’s assume that the leak can be revealed when some specific code path is followed. If the test team misses that path, it’ll be hard to find the leak.

  • A dedicated memory leak suite maintenance may need a separate test team. Also, you cannot include all the regression tests in the memory leak suite. It is simply because running a test multiple times and gathering those counters are both time-consuming and resource-consuming activities. So, it is recommended that you shuffle the test cases often and run your memory leak test suite.

  • When a new bug fix takes place, a test team verifies the fix using some test cases. Now you need to ask them whether those tests are already included in the memory leak test suite. If not, you need to include them. But if multiple fixes come in one day (say 10 or more), what to do? It may not be possible for you to include all the tests in your memory leak suite due to various reasons (for example, you may have resource constraints). Also, since a memory leak suite needs to run for hours, you get to see the result much later. So, in between, if new fixes enter into the main codebase, it is very hard to catch a leak that came earlier.

Summary

Preventing memory leaks in an application is a challenging task. It requires expertise and patience to find a leak in an application. In this chapter, I showed you the Diagnostic Tools window in Visual Studio and how to analyze a memory leak using events in C#.

This chapter answered the following questions:
  • What is a memory leak?

  • Why should you be careful about memory leaks?

  • What are the probable causes of memory leaks?

  • How can you perform a memory leak analysis with Visual Studio’s Diagnostic Tools window?

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.149.249.127