26 May, 2009

Catching memory leaks

Before return to discussion of other ways to catch "bad" pointers - I wanted to talk about memory leaks and tools for its diagnostics. As we'll see later, this topic is very close related to our previous talk.

While any error in your application is always bad, there are types of errors, which can be not visible in certain environments. For example, memory or resources leaks errors are relatively harmless on client machines and can be deadly on servers.

When newbie is inspired of the idea of catching memory leaks - he usually opens the Task Manager and watches "Mem usage" column:


So far so good, but then he suddenly notices that this column behaves very strange, even in a simple application: the memory is not decreasing when closing forms, but decreases at minimizing application, etc, etc. A good question: why does newbie think that this column represents memory allocated by his code? If you open "View"/"Select columns" menu - you'll see many other counters, which also matches "memory definition".

So, I'll open a little secret here: the "Mem Usage" column in Task Manager represents amount of RAM, occupied by your application. This value is not the memory, allocated by your code (you can figure out this by yourself, when you first encounter disappearing of memory at minimizing - of course, no one is going to free your memory without your permission). Your application can use less RAM, then your code allocates, since it can be swapped out to page file. And besides, RAM is spend for code too - namely, for system libraries. System libraries are loaded in every process, but there is only one copy of each DLL in system's memory! (I mean only code here). This value is also called "Working Set".

Here is a screenshot of more advanced task manager application - the Process Explorer; with enabled memory-related columns:


Large spread of values, isn't it?

For those, who want deeper explanation of memory in Windows, I recommend two articles by Mark Russinovich: Pushing the Limits of Windows: Virtual Memory and Pushing the Limits of Windows: Paged and Nonpaged Pool. See also this answer by Barry Kelly - AFAIK, he works on Delphi's compiler in Embarcadero.

So let leave our Task Manager for a while and take a look at Pascal. How Delphi manages its memory? All memory in Delphi application is controlled by memory manager. You can change the memory manager in your application by calling SetMemoryManager. That means that you can detect memory leaks very easy: all you need to do is to set your own memory manager. This manager will be just a stub, which redirects all requests to old (real) memory manager (you can use GetMemoryManager to get it) and just logs all memory usage.

What does it mean that your application has a memory leak? Well, this means that your code allocates some memory (object, string, array, etc) and forgets to release it. Forgetting about memory's block means that this memory will still be there at application's exit.

So, to catch all memory leaks you need to enumerate all busy memory blocks at application's exit. Every such block will represent a memory leak. Note, that you don't need to do this manually as there are plenty of ready-to-use tools available, which we are going to check out now...

Delphi

Okay, even in bare "out-of-the-box" Delphi there is basic abilities to diagnose memory leaks, so you can fast-check for leaks even without any external tools. But if your application indeed has memory leak - you still need some tool to identify it.

First of all, it is worth to note, that there were two versions of memory manager in Delphi's history. First one is very simple and old memory manager (the one, used in Delphi 7) and new versions of Delphi ship with modified version of FastMM memory manager. Standalone version of FastMM is one of the tools to catching memory leaks - more on it later.

Okay, there are AllocMemCount and AllocMemSize global variables in old memory manager - you can use them to get simple check for memory leaks. Just create the following unit in your project:
unit Unit2;

interface

implementation

uses
  Windows;

initialization

finalization

  if AllocMemCount <> 0 then
    MessageBox(0, 'An unexpected memory leak has occurred.', 'Unexpected Memory Leak', MB_OK or MB_ICONERROR or MB_TASKMODAL);

end.
Don't forget to include this unit first in your dpr-file. Now, if there is memory leak in your application - there will be a message box with error message at exit.

Yes, to find out the source of the memory leak - you need to use one of the tools, discussed below.

These variables are not available in recent Delphi's versions, but this functionality is collected by ReportMemoryLeaksOnShutdown flag. All you need to do in recent Delphi versions is to set this global variable to True at application's startup.

EurekaLog

EurekaLog has a functionality of catching memory leaks too. It is off by default - that because it is not free for your application. Enabling this functionality means a little slow down and increased memory usage. Besides, this functionality has its limits: it is not available in C++ Buider and application with run-time packages.

In any case, to enable it - you need to check "Catch memory leaks" option on "Advanced options" tab:


If you enable this option - there will be a usual error dialog at exit:

MS Classic style

EurekaLog style

Detailed report

As you can see: all memory leaks will be gathered in one single report, which can be send to you as any other EurekaLog report. The only differences from other kinds of reports are: no CPU and Assembler tabs and no calling of event handlers.

Few sub-options on the same "Advanced options" tab controls behavior of detecting memory leaks.

"Group son leaks with its father" option can increase readability of report with large amount of memory leaks. For example, there can be leaked object, which references another object. There will be two entries in the report without this option enabled. There will be only first object in the report with this option enabled. You can turn off this option to get more detailed reports.

"Hide Borland leaks" option tries to detect and ignore memory leaks by Delphi's code. Without this option you can get a mem-leak report - even if there is no memory leak in your own code.

"Free All Memory" option is quite obvious. There is no difference in most cases as all application's memory is released at exit. It is used to solve compatibility issues with other code.

We'll talk about "Catch leaks exceptions" option in the next time - it is the additional functionality that I've mentioned before. It'll help us to catch problems with objects.

FastMM

FastMM also have a good abilities for catching memory leaks. Unfortunately, Delphi's version of FastMM do not have full functionality, so you still need to download and install full version of FastMM. Actually, you don't need to "install" it: just download and unpack it.

Then you need to add FastMM4 unit in your application (may be you'll also need to add FastMM's folder in your search path). Of course, you need to add FastMM as very first unit in your application. If you are using EurekaLog - the ExceptionLog unit should be listed right after FastMM4 unit.

Right after installing FastMM is working in usual mode - as great memory manager. Different diagnostic options can be enabled by editing FastMM4Options.inc file - you need to change the options and make a build for your project. For those, who aren't very familiar with conditional defines:
{.$define AssumeMultiThreaded} - the option is turned OFF
{$define AssumeMultiThreaded} - the option is turned ON
Alternatively, you can use this visual tool to edit FastMM's options from friendly GUI application: just by checking and unchecking option's check-boxes.

So, first of all, you need to enable FullDebugMode option, as advanced diagnostic features are only available with FullDebugMode option enabled. Enabling this option on means that your application will require present of FastMM_FullDebugMode.dll library to run. The precompiled version can be found at \FullDebugMode DLL\Precompiled\ folder. You can also recompile it by yourself - more on this later. In simple case you only need to put this DLL in your application's folder or in C:\Windows\System32 folder, to make it available for all projects. Note, that mem-leaks functionality is also available without FullDebugMode option enabled, but this greatly limits its usefulness - as FastMM will behave as Delphi with ReportMemoryLeaksOnShutdown on (we discussed it above).

So, FastMM in debug mode is not very suitable for deploying application: you need to distribute an additional DLL. That is why the optimal way is to use FastMM in full debug mode while developing and testing, and to deploy an application without full debug mode of FastMM - optionally, enabling EurekaLog's mem-leaks feature. Yes, EurekaLog's looks modest, compared to FastMM, but this also makes it more suitable to use in end-client environments.

So, except for debug mode enabling, you need to enable functionality of memory leaks catching. It can be done by enabling EnableMemoryLeakReporting option.

Note, that near FullDebugMode and EnableMemoryLeakReporting options there are few other useful options listed (there is a good explanation of each option in the same file inside comments, so I'll only list them): RawStackTraces, LogErrorsToFile, LogMemoryLeakDetailToFile, ClearLogFileOnStartup, DisableLoggingOfMemoryDumps, HideExpectedLeaksRegisteredByPointer, RequireIDEPresenceForLeakReporting, RequireDebuggerPresenceForLeakReporting, RequireDebugInfoForLeakReporting and ManualLeakReportingControl.

Besides, there are other interesting options as: NoMessageBoxes and UseOutputDebugString. BTW, you can catch output of OutputDebugString by using Delphi's debugger (View/Debug Windows/Events) or by using DebugView.

CheckHeapForCorruption, CatchUseOfFreedInterfaces and DetectMMOperationsAfterUninstall options will be discussed in the next time, as they are directly related to our previous topic.

Ok, there is one more step left: you need to add a debug information to your application. In the previous case, the debug information is being generated and injected by EurekaLog's expert. And now you have to include it manually. The debug info is used by FastMM_FullDebugMode.dll library while generating call stacks for bug reports, so if you want to have nice "Button1Click" instead of just raw addresses - you'd better to use some debug info.

By default, the FastMM_FullDebugMode.dll is compiled against JCL library, which means that default DLL has support for map, TD32 and JDBG sources of debug info. You can recompile this library to support EurekaLog's debug info. To do that, you need to disable JCLDebug option and enable EurekaLog option in FastMM_FullDebugMode.dpr file and recompile this project. After that you only need to enable EurekaLog for your project and FastMM will use its debug info.

If you don't want or can't recompile debug DLL, then you can add other source of debug info to your application. Map files and TD32 debug info is supported by Delphi itself. The first one is enabled by switching "Map file" option to "Detailed" - it is on "Linker" tab. And the second case - by enabling "Include TD32 debug info" option (it is called "Debug information" in D2009 and later) on the same "Linker" tab in project's options. The first case means that there will be a separate map-file, which you need to distribute with your executable. The second case means injecting info in the executable. Unfortunately, these formats are not suitable for end-user environments as they drammatically increases size of your application.

The most compact case is debug information in EurekaLog's or JCL's formats. I've already described the first one. As for the second - you need to download and install the JCL library (there is an automatic installer for it - just run the Install.bat file) and enable JDBG debug information for the project:


I suppose that you may want to keep all options enabled.

So, the full entry about one single memory leak looks like this:
--------------------------------2009/5/26 21:52:35--------------------------------

A memory block has been leaked. The size is: 12

This block was allocated by thread 0x4F8C, and the stack trace (return addresses) at the time was:

403206 [System][System.@GetMem]
4CC5B7 [Unit15.pas][Unit15][Unit15.B][35]
4CC5C4 [Unit15.pas][Unit15][Unit15.A][39]
4CC5DC [Unit15.pas][Unit15][Unit15.TForm15.FormCreate][43]
4C0CCB [Forms][Forms.TCustomForm.DoCreate]
4C0913 [Forms][Forms.TCustomForm.AfterConstruction]
4C08E8 [Forms][Forms.TCustomForm.Create]
4CA539 [Forms][Forms.TApplication.CreateForm]
4CD986 [Project14][Project14.Project14][14]
75B94911 [BaseThreadInitThunk]
770FE4B6 [Unknown function at RtlInitializeExceptionChain]

The block is currently used for an object of class: TTest
The allocation number is: 3814

Current memory dump of 256 bytes starting at pointer address 7FF75E50:

A0 C5 4C 00 E8 5E F7 7F 00 00 00 00 F4 20 51 2A 00 00 00 00 E0 4B F7 7F 00 00 00 00 00 00 00 00
FF FF FF FF 00 00 00 00 E7 0E 00 00 06 32 40 00 B7 C5 4C 00 C4 C5 4C 00 DC C5 4C 00 CB 0C 4C 00
13 09 4C 00 E8 08 4C 00 39 A5 4C 00 86 D9 4C 00 11 49 B9 75 B6 E4 0F 77 8C 4F 00 00 22 32 40 00
EF 70 42 00 26 6B 42 00 D9 6A 42 00 5A 9B 42 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 8C 4F 00 00 08 00 00 00 00 00 00 00 81 6F AF 70 0C 11 40 00 00 00 00 00
7E 90 50 8F 80 80 80 80 00 00 00 00 E0 4B F7 7F 00 00 00 00 00 00 00 00 FF FF FF FF 00 00 00 00
E8 0E 00 00 06 32 40 00 DC C5 4C 00 CB 0C 4C 00 13 09 4C 00 E8 08 4C 00 39 A5 4C 00 86 D9 4C 00
11 49 B9 75 B6 E4 0F 77 89 E4 0F 77 00 00 00 00 8C 4F 00 00 00 00 00 00 00 00 00 00 00 00 00 00

Е  L  .  и  ^  ч    .  .  .  .  ф     Q  *  .  .  .  .  а  K  ч    .  .  .  .  .  .  .  .
я  я  я  я  .  .  .  .  з  .  .  .  .  2  @  .  ·  Е  L  .  Д  Е  L  .  Ь  Е  L  .  Л  .  L  .
.  .  L  .  и  .  L  .  9  Ґ  L  .  †  Щ  L  .  .  I  №  u  ¶  д  .  w  Њ  O  .  .  "  2  @  .
п  p  B  .  &  k  B  .  Щ  j  B  .  Z  ›  B  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .  .
.  .  .  .  .  .  .  .  Њ  O  .  .  .  .  .  .  .  .  .  .  Ѓ  o  Ї  p  .  .  @  .  .  .  .  .
~  ђ  P  Џ  Ђ  Ђ  Ђ  Ђ  .  .  .  .  а  K  ч    .  .  .  .  .  .  .  .  я  я  я  я  .  .  .  .
и  .  .  .  .  2  @  .  Ь  Е  L  .  Л  .  L  .  .  .  L  .  и  .  L  .  9  Ґ  L  .  †  Щ  L  .
.  I  №  u  ¶  д  .  w  ‰  д  .  w  .  .  .  .  Њ  O  .  .  .  .  .  .  .  .  .  .  .  .  .  .

MemProof/AQTime

This two tools are professional profilers, which can diagnose not only problems with memory leaks, but detect leaks of other types of resources. MemProof is (free) predecessor of AQTime. It is no longer supported, but you still can found it on the Internet.

I won't cover these tools here, as it is a large and separate topic. I've mentioned them here only as example of more powerful mem-leak debugging tools than EurekaLog or FastMM are.

Not just mem-leaks...

As I've mentioned few times before, the tools for diagnosing memory leaks has side-functionality, which can catch other types of memory misuses, which can lead to Access Violation errors. Note, that Access Violation is a very tricky error: it can not show yourself in the buggy code. Here is simplest example:
var
  Str: TStringList;
begin
  Str := TStringList.Create;
  try
    Str.Add('Test 1');
  finally
    Str.Free;
  end;
  Str.Add('Test 2');      // Error! Accessing the already deleted object
  ShowMessage(Str.Text);
end;
Though this code has a bug, but there is a very good probability that it'll work for you without raising any kind of exception and will display a result's message.

But we'll talk about that in the next time.

P.S. Oh, yes, one more thing: do not forget, that, no matter what tool you do use, you always can get a better results from it by adjusting your project's options ;)

No comments:

Post a Comment

You can use some HTML-tags like:

<b>Bold</b>
<i>Italic</i>
<a href="http://www.example.com/">Link</a>