Jump to content

Identificando Memory Leaks Em Uma Aplicação Delphi usando o FastMM4


FabioCorrea

Postagens Recomendadas

  • Pessoal da TecnoSpeed

Durante o desenvolvimento e manutenção de sistemas de software, por melhor que os desenvolvedores possam ser, erros podem ser cometidos. Um dos erros mais difíceis de serem detectados são os chamados Memory Leaks, principalmente por não causarem erros aparentes na execução dos programas, mas nem por isso são menos destrutivos quando não tratados.

Memory Leaks, ou “Vazamento de Memória”, é um problema que ocorre quando porções de memória utilizadas pelo programa não são liberadas depois de utilizadas. Isso faz com que o programa passe a usar mais e mais memória, nunca liberando, e pode por fim consumir toda a memória RAM, causando graves problemas ao sistema (ex: encerramento abrupto, travamentos, lentidão, erros de execução, etc). Esse problema é particularmente grave em sistemas que são executados por longos períodos de tempo, como serviços, aplicações para servidores, drivers de dispositivos, sistemas embarcados, etc.

Portanto, vamos demonstrar como podemos capturar esse problema em nossas aplicações Delphi, usando a ferramenta FastMM4 (https://github.com/pleriche/FastMM4). Usarei neste artigo o Delphi XE com o FastMM4.

O primeiro passo é baixar o FastMM4, através do Git ou mesmo baixando como arquivo .zip. Como exemplo, eu baixei o .zip e extraí na pasta C:\Desenvolvimento\FastMM4:

image.png

Devemos fazer alguns ajustes em um arquivo arquivo “include” do próprio FastMM4. Na pasta de instalação do FastMM4 edite o arquivo FastMM4Options.inc. Nele há “defines” (diretivas de compilador) para ativar e desativar as várias funcionalidades do FastMM4, incluindo a de detecção de memory leaks. Procure pela linha que contém o define para “FullDebugMode”. Por padrão esse define vem desativado com um ponto na frente, dessa forma:

{.$define FullDebugMode}

Remova o ponto para ativar a função de detecção de memory leaks:

{$define FullDebugMode}

Outro ajuste a ser feito, para possibilitar a detecção de memory leaks em projetos de DLLs, é desativar o define “RequireDebuggerPresenceForLeakReporting”. Procure por ele e inclua um ponto antes do “$”, assim:

{.$define RequireDebuggerPresenceForLeakReporting}

Salve o arquivo. Vale notar que é necessário fazer isso uma única vez.

Será necessário copiar a DLL do FastMM4 (FastMM_FullDebugMode.dll) para a pasta de saída do executável do seu projeto Delphi, que se encontra em FastMM4\FullDebugMode DLL\Precompiled:

Clipboarder.2022.05.06-007.png

No projeto de exemplo que estarei disponibilizando em anexo a esse artigo, eu configurei a pasta de saída (Output Directory) como vazio, ou seja, o executável será gerado na própria pasta do projeto, portanto é nele que você deve deixar a DLL do FastMM4.

Agora vamos configurar o projeto Delphi para trabalhar com o FastMM4. Primeiro precisamos adicionar a pasta do FastMM4 ao Search Path. No Delphi, entre no menu Project → Options, na lista lateral selecione Delphi Compiler, depois Search Path e clique no botão “...”, e faça a inclusão da pasta:

Clipboarder.2022.05.06-011.png

Em seguida, em Compiling, configure da seguinte forma: Debug information = true; Use Debug .dcus = true.

Clipboarder.2022.05.06-013.png

Agora em Linking, configure: Debug information = true; Map file = Detailed. Clique em OK para confirmar todos esses ajustes.

Clipboarder.2022.05.06-014.png

Vamos agora incluir a unit do FastMM4 ao uses do próprio projeto. Para isso, entre no menu Project → View Source. Inclua então a FastMM4 como o primeiro uses do projeto. É muito importante que seja o primeiro.

Clipboarder.2022.05.06-012.png

Por fim, devemos “rebuildar” completamente a aplicação. Entre no menu Project → Build <nome do projeto>, ou “Build All Projects”.

Nossa aplicação agora está pronta para identificar possíveis memory leaks que aconteçam durante sua execução. Se um memory leak acontece, ao encerrar a aplicação um alerta é exibido e um relatório de memory leaks em formato .txt é gerado na pasta do executável. Caso não ocorram memory leaks, não é levantado alerta nem gerado relatório (inclusive se já existir um relatório ele é apagado).

Vamos causar um memory leak na nossa aplicação prá demonstrar como funciona. Como exemplo, incluí um botão no form do projeto e usei o seguinte código:

procedure TfrmDemoFastMM4.btnMemoryLeakClick(Sender: TObject);
var
  _sl: TStringList;
begin
  _sl := TStringList.Create;
  try
    { ... }
  finally

  end;
end;

Rodamos a aplicação e clicamos no botão. Ao encerrar a aplicação, o alerta de memory leak é exibido, e o relatório de memory leaks é gerado na pasta do executável:

Clipboarder.2022.04.22-007.png

Clipboarder.2022.05.13-003.png

Incluirei um relatório de memory leaks de exemplo junto a esse artigo. Eles costumam ser extensos e conter muita informação que não normalmente usamos (como memory dumps), mas vamos aprender como “navegar” nesse relatório e encontrar os pontos que nos dizem quais e onde estão os pontos que estão causando memory leaks.

O primeiro ponto que devemos prestar atenção é no final, onde ele relata quais foram os “leaks”, normalmente os nomes das classes que foram instanciadas e não foram liberadas:

This application has leaked memory. The small block leaks are (excluding expected leaks registered by pointer):

85 - 100 bytes: TStringList x 1

Voltando ao início do relatório, vemos que ele detalha todas as ocorrências de memory leaks. Como nosso projeto gerou apenas 1 memory leak, temos somente 1 item com a stack trace do leak, semelhante a isso:

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

This block was allocated by thread 0x328C, and the stack trace (return addresses) at the time was:
40455E [System.pas][System][@GetMem][3693]
4050A7 [System.pas][System][TObject.NewInstance][11044]
405622 [System.pas][System][@ClassCreate][12121]
442382 [Classes.pas][Classes][TStringList.Create][6128]
765F884F [GetWindowLongW]
765FE9C6 [NotifyWinEvent]
4B31CC [uMain.pas][uMain][TfrmDemoFastMM4.btnMemoryLeakClick][38]
4722AD [Controls.pas][Controls][TControl.Click][7190]
49A10B [StdCtrls.pas][StdCtrls][TCustomButton.Click][4562]
49ABF9 [StdCtrls.pas][StdCtrls][TCustomButton.CNCommand][5023]
471D3F [Controls.pas][Controls][TControl.WndProc][7074]

The block is currently used for an object of class: Classes.TStringList

O que devemos tentar encontrar aqui é “algo familiar”, ou seja, o nome de uma unit ou classe particular de nosso projeto. Aqui é necessário estar bastante familiarizado com o projeto, conhecendo o nome de suas units, classes, etc.

Oras, como nossa unit chama-se uMain.pas, então podemos notar a seguinte linha:

4B31CC [uMain.pas][uMain][TfrmDemoFastMM4.btnMemoryLeakClick][38]

Isso significa que na unit uMain.pas, na rotina TfrmDemoFastMM4.btnMemoryLeakClick, mais exatamente na linha 38 da unit, houve uma instância da classe TStringList que foi criada mas não foi liberada. Ou seja, ele se refere exatamente a essa linha:

_sl := TStringList.Create;

Se corrigirmos o memory leak, por exemplo, adicionando _sl.Free; ao finally, o memory leak deixa de acontecer, e nossa aplicação encerra sem alerta de memory leak e sem relatório.

Aplicações grandes e complexas, com muitos memory leaks, costumam gerar extensos relatórios, na casa de dezenas de megabytes, e que pode demorar até vários minutos para serem gerados no encerramento da aplicação. 

Mas calma: nem sempre relatórios extensos significam um alto número de memory leaks. Muitas vezes o que pode estar acontecendo é que leaks podem estar acontecendo com instâncias de classes complexas, que contém muitas subclasses que por sua vez contém mais subclasses. Todas essas subclasses acabam sendo listadas no relatório como leaks. Portanto, uma grande sequência de leaks podem ser causadas pelo esquecimento de liberar uma única instância. É comum nesses casos matar vários leaks com um único .Free!

É bom ter uma boa dose de paciência e persistência durante a caça aos memory leaks! Mas o resultado é uma aplicação mais estável e eficiente, portanto compensa muito o esforço.

O exemplo acima ainda é realmente bastante simples, então vamos mostrar algo um pouco mais complexo, para dar uma noção melhor de como rastrear um leak.

Em um outro botão na nossa aplicação de exemplo, criei uma subrotina, e criei também um outro tipo de memory leak muito comum: a instanciação múltipla.

procedure SubRotina;
var
  _sl: TStringList;
begin
  _sl := TStringList.Create;
  _sl := TStringList.Create;
  try
    { ... }
  finally
    _sl.Free;
  end;
end;

procedure TfrmDemoFastMM4.btnLeakSubrotinaClick(Sender: TObject);
begin
  SubRotina;
end;

No relatório gerado, vemos uma stack trace semelhante a essa:

40455E [System.pas][System][@GetMem][3693]
4050A7 [System.pas][System][TObject.NewInstance][11044]
405622 [System.pas][System][@ClassCreate][12121]
442382 [Classes.pas][Classes][TStringList.Create][6128]
4B3208 [uMain.pas][uMain][SubRotina][50]
4B3259 [uMain.pas][uMain][TfrmDemoFastMM4.btnLeakSubrotinaClick][61]
4722AD [Controls.pas][Controls][TControl.Click][7190]
49A10B [StdCtrls.pas][StdCtrls][TCustomButton.Click][4562]
49ABF9 [StdCtrls.pas][StdCtrls][TCustomButton.CNCommand][5023]
471D3F [Controls.pas][Controls][TControl.WndProc][7074]
7782FCBB [RtlRegisterSecureMemoryCacheCallback]

Em uma stack trace a ordem dos acontecimentos ocorre de baixo para cima. Portanto, ao chegarmos na parte “familiar” (uMain.pas), vemos que ele indica a linha 61, mas não foi nela que ocorreu o leak, mas sim a chamada à SubRotina, que essa sim foi quem causou o leak, na linha 50.

Notamos nesse trecho de código que apesar de termos liberado a instância _sl, ela inicialmente recebeu duas vezes uma instância de TStringList. Portanto, nesse caso o que devemos fazer é eliminar a dupla instanciação para corrigirmos o memory leak.

É possível também realizar a detecção de memory leaks em bibliotecas DLL. Para isso devemos fazer as mesmas configurações de um projeto normal (para executáveis). Nesse artigo também estou incluindo um projeto de DLL de exemplo bem simples, com apenas 1 rotina que faz apenas instanciar uma TStringList, mas não a libera a fim de causar um memory leak. Na aplicação de exemplo inclui um botão que chama a rotina da DLL. 

Mas existe uma diferença no momento em que o relatório é gerado, dependendo da forma como a DLL é carregada. Se é carregada estaticamente o relatório é gerado no encerramento da aplicação, mas se é carregada dinamicamente o relatório é gerado ao se liberar a biblioteca (no Delphi, é usualmente com a rotina FreeLibrary). Em ambos os casos o relatório gerado é separado para a DLL, com o nome correspondente a ela e contendo informações somente sobre a DLL.

Na aplicação de exemplo é possível escolher a forma de carregamento da DLL com a diretiva STATIC. Caso ativa, o carregamento é estático, caso contrário é dinâmico. Portanto em nossa aplicação, na sua forma final, temos 3 botões, ao clicar em cada um, temos no final um relatório para o executável contendo 2 memory leaks, e um relatório para a DLL com 1 memory leak.




 

DemoFastMM4.zip

DemoFastMM4_MemoryManager_EventLog.txt

  • Amei 2
Link to comment
Compartilhe em outros sites

Crie uma conta ou entre para comentar 😀

Você precisa ser um membro para deixar um comentário.

Crie a sua conta

Participe da nossa comunidade, crie sua conta.
É bem rápido!

Criar minha conta agora

Entrar

Você já tem uma conta?
Faça o login agora.

Entrar agora
  • Quem está online   0 Membros, 0 Anônimos, 27 Visitantes (Ver lista completa)

    • There are no registered users currently online


×
×
  • Create New...