标题: Delphi Memory Monitor (转载) Delphi Memory Monitor
The article introduces the following topics
How the Manager of Dynamic Memory works in Delphi applications
How to reveal errors leading to memory leaking
How to use Memory Mapped Files for inter-process communications
Content
Introduction
Memory manager in Delphi
Why a stand-alone memory monitor?
Memory mapped files
Monitor
Test application
Introduction
Errors in allocating and freeing dynamic memory are known as the most difficult to find out. They normally don't affect behaviour of application and can be easily missed during testing. This problem is often referred to as ?memory leaking?.
In case of a small utility, which having started does some job and exits, memory allocation errors are not a big problem. System frees applications memory automatically. On the other hand if an application is supposed to work for long time memory leaking can cause crash of the application or even operating system itself. It is a true example for non-interactive service.
The system described in this article automates the process of finding this kind of errors in Delphi projects.
Memory Manager in Delphi
Delphi memory manager consists of three system functions. They are GetMem, FreeMem and ReallocMem. Any action leading to allocating or freeing memory is eventually translated to a call to one of these functions. It means when you call new(AnyVariable) or AnyObject.Create, necessary memory will be allocated by calling GetMem. In the same way dispose(AnyVariable) and AnyObject.free will use FreeMem to deallocate memory.
Nevertheless Delphi doesn?t call these functions directly. Instead it refers to the global variable MemoryManager of TmemoryManager type, which is declared in the system.pas as following
TMemoryManager = record
GetMem: function(Size: Integer): Pointer;
FreeMem: function(P: Pointer): Integer;
ReallocMem: function(P: Pointer; Size: Integer): Pointer;
end;
MemoryManager contains pointers to the corresponding functions. During startup it is initialised with the pointers to original Delphi functions. This approach makes it possible to write our own functions and replace the original ones. Delphi provides three functions to manipulate with the memory manager.
Procedure GetMemoryManager returns the entry points of the currently installed memory manager.
Procedure SetMemoryManager installs a new memory manager.
Function IsMemoryManagerSet returns true if any of the original functions has been replaced and false otherwise.
To control the use of dynamic memory we need to count all requests to allocate and deallocate memory blocks. Functions NewGetMem, NewFeeMem and NewReallcoMem shown in listing in figure 1 increment counter and call the same function from the previously installed memory manager. Procedure SetNewMemMgr installs the new memory manager.
Unit DMMCL
type
PDMM=^TDMM;
TDMM=record
getmemcount:integer;
freememcount:integer;
end;
var
fp: TDMM;
OldMemMgr: TMemoryManager;
function NewGetMem(Size: Integer): Pointer;
begin
Inc(fp.GetMemCount);
Result := OldMemMgr.GetMem(Size);
end;
function NewFreeMem(P: Pointer): Integer;
begin
Inc(fp.FreeMemCount);
Result := OldMemMgr.FreeMem(P);
end;
function NewReallocMem(P: Pointer; Size: Integer): Pointer;
begin
result:=OldMemMgr.ReallocMem(p,size);
if (p<>nil) then begin
Inc(fp.FreeMemCount); //freemem
if size>0 then Inc(fp.GetMemCount); //getmem
end else begin
if size>0 then Inc(fp.GetMemCount); //getmem
end;
const
NewMemMgr: TMemoryManager = (
GetMem: NewGetMem;
FreeMem: NewFreeMem;
ReallocMem: NewReallocMem);
Procedure SetNewMemMgr;
begin
fp.getmemcount:=0;
fp.freememcount:=0;
GetMemoryManager(OldMemMgr);
SetMemoryManager(NewMemMgr);
end;
Initialization
SetNewMemMgr;
End.
Figure1 ? the DMMCl unit
Now we can get to know how many blocks of memory were allocated and freed by the application and see if the application works correctly with dynamic memory. Generally every object, function and application itself must free all the memory it allocated. For example comparing the number of calls to GetMem and FreeMem before creating an object and after destroying it we can check whether the object has returned all allocated memory.
Why a stand-alone memory monitor?
We could stop on this and it would make sense for small projects. Debugging the application you can bring variables fp.GetMemCount and fp.FreeMemCount to the watch window and monitor them while executing different part of code. Another possibility is to create a form in a debugged application and display the counters.
But we will go further and create a stand-alone application-monitor, which will continuously show the number of calls to memory manager functions in other applications. Stand-alone application has some important advantages
1. No need to duplicate the same code in all applications
2. It can collect information from many applications in one place and present it in convenient format
3. An application being debugged may become unable to display information
4. If a debugged application is non-interactive service or console type application it may be undesirable or impossible to create a form in it
We need a mechanism to pass information from debugged applications to the application-monitor. The simplest and most effective way in our case would be a memory-mapped file.
Memory Mapped Files
Memory mapping objects provide easy way to access files by mapping them into the address space of application. Application can write data to and read from a memory-mapped file through any pointer. Many applications can share the same memory mapped object and thus share data in memory. If one process changes data the changes are immediately visible for other processes. Memory mapped file can be permanent or temporary. Permanent means that the file to be mapped exists in the file system and will remain after being closed. Temporary file is created by the operating system when the application creates a memory-mapping object and automatically deleted when all handlers are closed.
The following steps should be performed before using memory-mapped file
1. Open a file by CreateFile function. For temporary files this is not needed.
2. Create a file-mapping object by CreateFileMapping function.
3. Map the file into memory by MapViewOfFile function.
We use temporary files because the client application only needs them to share data with the monitor when it is running. Therefore we skip the step 1 and go to creating file mapping.
FileHandler:Thandle;
FileName:string;
FileHandler:=CreateFileMapping($ffffffff,nil,PAGE_READWRITE,0,sizeof(TDMM), pchar(FileName));
The first parameter is the handle to a file. Value $ffffffff means that the file is temporary and must be created in the operating system paging file. The third parameter, PAGE_READWRITE specifies that the file will be opened with read write access. The fourth and fifth parameters are high and low four bytes of the maximum size of the file-mapped object. The last parameter specifies the name of the mapped object. The name must be unique in the scope of operating system because the file mapped objects are global and they share the same memory space with the other global objects like mutexes, events, etc. The name also works as an identifier when an application opens a mapped object created by other application. If the function succeeds it returns a handle to the memory mapped object.
fp:=MapViewOfFile(fmh,FILE_MAP_WRITE,0,0,0);
The first parameter is the handle returned by the CreateFileMapping function. The second parameter specifies the type of access to the file. The other three parameters are offset and number of bytes to map. To map the entire file all of these parameters should be zero.
Every EXE and DLL module to be debugged creates a separate memory mapped file. Files are named DelphiMM0, DelphiMM1 and so on to make them unique. We will limit the number of client applications, which can be monitored at a time by 10. Function RegisterApp is shown in listing in figure 2. Complete source code can be found in accompanying files.
type
PDMM=^TDMM;
TDMM=record
appname:array[0..255] of char;
modname:array[0..255] of char;
getmemcount:integer;
freememcount:integer;
end;
const
TempFileName='DelphiMM';
MaxClientNumber=10;
var
mfilename:string;
function RegisterApp:boolean;
var
i:integer;
begin
i:=1;
repeat
mfilename:=TempFileNAme+inttostr(i);
//try to create a new file. $ffffffff for first parameter
//means temporary file
fmh:=CreateFileMapping($ffffffff,nil,PAGE_READWRITE,0,sizeof(TDMM),
pchar(mfilename));
result:=(fmh<>0) and (getLastError<>ERROR_ALREADY_EXISTS);
inc(i);
until result or (i>MaxClientNumber);
if result then begin //file successfuly created
fp:=MapViewOfFile(fmh,FILE_MAP_WRITE,0,0,0);
result:=fp<>nil;
if result then begin
GetModuleFileName(hinstance,fp.modname,254);
//fp.modname contains name of current module. if code is in a dll
//we will get the dll name. Thus we can debug DLL's separately
GetModuleFileName(GetModuleHandle(nil),fp.appname,254);
//fp.appname contains name of exe file.
SetNewMemMgr();
end;
end else
MessageBox(0,'Memory manager cannot be installed','Error',MB_ICONERROR);
end;
figure 2 ? the RegisterApp function
First, in repeat loop we go from 1 to 10 to find a name, which has not been yet taken by other client applications. If a ?free name? has been found we create a temporary memory-mapped file and install a new memory manager thus making the application visible by the monitor.
Two more fields have been added to the TDMM record: modname and appname. Appname contains the name of the main file of the application and modname contains the name of the module whose code is being executed. If executed code resides in the main executable file these two fields will have the same value. If code is executed from a DLL the modname field will contain the name of the DLL.
Add the Unit DMMCL to any project (menu Project-Add to Project) and the client application is ready.
Monitor
The application-monitor periodically reads all files and displays information for every module being debugged.
We need a form that contains Ttimer, TlistView and Tbutton components as it is shown in figure 3.
Figure 3 ? the main form of the monitor
ListView1 component has 5 columns. They contain name of debugged application, status (running or finished), number of allocated memory blocks, number of freed blocks and difference between them.
The most of the job is done in the timer?s OnTime event handler routine.
It tries to open memory mapping objects, created by clients by OpenFileMapping function.
fmh:=OpenFileMapping(FILE_MAP_READ, false, pchar(TempFileName+inttostr(i)));
This function only succeeds if the memory mapping object named pchar(TempFileName+inttostr(i)) exists. If the object exists and is not in the list, a new record will be created. If the object doesn?t exists but the corresponding record exists it means that the client application exited. In this case the record will still remain with the status ?finished? until the user explicitly delete it. To delete all records of finished applications push the ?Delete finished? button.
Figure 4 shows timer event handler.
procedure TForm1.Timer1Timer(Sender: TObject);
var
i:integer;
ListItem:TListItem;
dispname:string;
begin
i:=1;
repeat
//TListView1.findData locates a list view item with
//its Data property equal to the Value parameter
//If a file number i has already been picked up
//previously ListItem will refer to a TListItem assosiated
//with tis file. Otherwise ListItem will be equal to nil
ListItem:=ListView1.findData(0,pointer(i),true,false);
//try to open memory mapped file
fmh:=OpenFileMapping(FILE_MAP_READ, // access mode
false, // inherit flag
pchar(TempFileName+inttostr(i)));
if fmh=0 then begin //file doesn't exist
//check if file existed during last scanning, which means application
//exited and we should update its status in ListView
if ListItem<>nil then begin
ListItem.data:=nil;
ListItem.SubItems[0]:='Finished';
end;
end else begin
fp:=MapViewOfFile(fmh,FILE_MAP_READ,0,0,0);
if fp=nil then begin
end;
if ListItem=nil then begin //new application or DLL
ListItem:=ListView1.items.Add;
//Name of application
dispname:=extractfilename(fp.appname);
//If module name differs from application name make
//composed name like App_name->Mod_Name
if (dispname<>ExtractFileName(fp.modname)) then
dispname:=dispname+'->'+ExtractFileName(fp.modname);
ListItem.Caption:=dispname;
ListItem.Data:=pointer(i);
ListItem.SubItems.Add('Running');
ListItem.SubItems.Add(inttostr(fp.getmemcount));
ListItem.SubItems.Add(inttostr(fp.freememcount));
ListItem.SubItems.Add(inttostr(fp.getmemcount-fp.freememcount));
end else begin //item for this application already exists, update it
ListItem.SubItems[1]:=inttostr(fp.getmemcount);
ListItem.SubItems[2]:=inttostr(fp.freememcount);
ListItem.SubItems[3]:=inttostr(fp.getmemcount-fp.freememcount);
end;
UnmapViewOfFile(fp);
CloseHandle(fmh);
end;
inc(i);
until (i>MaxClientNumber);
end;
Figure 4 ? the timer event handler procedure
Complete source code and executable file of application monitor can be found in accompanying zip file.
To demonstrate how the system works we will create a simple client application DMMtest. Create a new project, which contains two forms, Form1 and Form2. The main form Form1 is shown in figure 5.
figure 5 ? the main form of the test application
It contains five buttons with the following OnClick event handlers
var
AnyObject:Tobject;
AnyPointer:^real;
procedure TForm1.Button1Click(Sender: TObject);
begin
AnyObject:=TList.Create;
end;
procedure TForm1.Button2Click(Sender: TObject);
begin
AnyObject.Free;
end;
procedure TForm1.Button3Click(Sender: TObject);
begin
new(AnyPointer);
end;
procedure TForm1.Button4Click(Sender: TObject);
begin
dispose(AnyPointer);
end;
procedure TForm1.Button5Click(Sender: TObject);
begin
form2:=TForm2.Create(nil);
form2.Show;
end;
Another form, Form2 has only OnClose event handler
procedure TForm2.FormClose(Sender: TObject; var Action: TCloseAction);
var
i:integer;
begin
Action:=caFree;
end;
Add the Unit DMMCL to the project(menu project-Add to Project)and compile it. Start monitor DELPHIMM.EXE then run the testing project. When the application starts a new line should appear in the list (see figure 6).
Figure 6 ? the test application being debugged
It shows number of calls to GetMem and FreeMem.
Try to push the ?New? button. ?GetmemCount? and ?Diff? should be incremented by 1. If you press the ?Dispose? button ?Freememcount? value is decremented by 1 and difference gets the same value as it was before you pressed the ?New? button. Try to push ?Create object? and ?Free object? buttons. Again ?GetmemCount? and ?Freememcount? will be incremented by the same value and difference will remain. Note the value of ?Diff? and press the ?Open Form? button to create a new form. You can see how many blocks of memory an empty form allocates. Close the form and see if it has deallocated all memory.
As it was mentioned previously we could also debug DLLs. Let?s create a simple DLL, which imports 2 functions. Create a new DLL project (manu File,New,DLL) TestDLL and copy the code from the listing in Figure 7 to the main file. Compile the project and copy testDLL.dll to the folder in which you created DMMtest project. Open DMMtest project and include the following declarations in the unit of the main form
procedure CreateObj; stdcall;
procedure FreeObj; stdcall;
procedure CreateObj; external 'testdll.DLL';
procedure FreeObj; external 'testdll.DLL';
library testdll;
uses dmmcl,classes;
var
AnyObject:TObject;
procedure DLLCreateObj; stdcall;
begin
AnyObject:=TObject.Create;
end;
procedure DLLFreeObj; stdcall;
begin
AnyObject.free;
end;
exports
DLLCreateObj index 1,
DLLFreeObj index 2;
begin
end.
Figure 7 source code of sample DLL
Here are OnClick event handlers.
procedure TForm1.Button6Click(Sender: TObject);
begin
DLLCreateObj;
end;
procedure TForm1.Button7Click(Sender: TObject);
begin
DLLFreeObj;
end;
Figure 8 - the test client application with two more buttons for DLL functions
Start the monitor and run the testing project. Now two lines should appear in the list, one for the application and one for the DLL as it is shown in figure 9. Press ?DLL create object? and ?DLL free object? to execute functions from the DLL.
Figure 9 ? The monitor shows the main file and the DLL separately.
Conclusion
In this article we have seen how the memory manager works in delphi applications and learnt how memory mapped files can be used for inter-process communication. Even though there are many things about these complicated subjects left out of the scope of the article, the techniques described here can help you make you programs more efficient and stable.
About the author.
Works as a senior software developer for Pessl Instruments Ltd, Austria. Used Borland Delphi for more then 6 years to develop communications, speech recognision and other types of software for different platforms.
Written by Vitaly Ignatovich - http://
This article has been seen 13878 times.