Database
 sql >> Base de Dados >  >> RDS >> Database

Lidando com um vazamento de recursos GDI


Vazamento de GDI (ou, simplesmente, o uso de muitos objetos GDI) é um dos problemas mais comuns. Eventualmente, causa problemas de renderização, erros e/ou problemas de desempenho. O artigo descreve como depuramos esse problema.

Em 2016, quando a maioria dos programas são executados em sandboxes de onde mesmo o desenvolvedor mais incompetente não pode prejudicar o sistema, fico surpreso ao enfrentar o problema sobre o qual falarei neste artigo. Francamente falando, eu esperava que esse problema tivesse ido para sempre junto com o Win32Api. Mesmo assim, enfrentei. Antes disso, eu só ouvia histórias de horror sobre isso de desenvolvedores antigos e mais experientes.

O problema


Vazamento ou uso da enorme quantidade de objetos GDI.

Sintomas

  1. A coluna de objetos GDI na guia Detalhes do Gerenciador de Tarefas mostra 10.000 críticos (se esta coluna estiver ausente, você pode adicioná-la clicando com o botão direito do mouse no cabeçalho da tabela e selecionando Selecionar Colunas).
  2. Ao desenvolver em C# ou em outras linguagens executadas pelo CLR, ocorre o seguinte erro pouco informativo:
    Mensagem:Ocorreu um erro genérico no GDI+.
    Fonte:System.Drawing
    TargetSite:IntPtr GetHbitmap(System.Drawing.Color)
    Tipo:System.Runtime.InteropServices.ExternalException
    O erro pode não ocorrer com determinadas configurações ou em determinadas versões do sistema, mas seu aplicativo não poderá renderizar um único objeto:
  3. Durante o desenvolvimento em С/С++, todos os métodos GDI, como Create%SOME_GDI_OBJECT%, começaram a retornar NULL.

Por quê?


Os sistemas Windows não permitem a criação de mais de 65535 objetos GDI. Esse número, de fato, é impressionante e dificilmente consigo imaginar um cenário normal exigindo uma quantidade tão grande de objetos. Há uma limitação para processos – 10.000 por processo que pode ser modificado (alterando o HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Windows\GDIProcessHandleQuota valor no intervalo de 256 a 65535), mas a Microsoft não recomenda aumentar essa limitação. Se você ainda fizer isso, um processo poderá congelar o sistema para que ele não consiga renderizar nem mesmo a mensagem de erro. Nesse caso, o sistema pode ser reativado somente após a reinicialização.

Como corrigir?


Se você está vivendo em um mundo CLR confortável e gerenciado, há uma grande chance de você ter um vazamento de memória comum em seu aplicativo. O problema é desagradável, mas é um caso bastante comum. Há pelo menos uma dúzia de ótimas ferramentas para detectar isso. Você precisará usar qualquer criador de perfil para ver se o número de objetos que agrupam recursos GDI (Sytem.Drawing.Brush, Bitmap, Pen, Region, Graphics) aumenta. Se for o caso, você pode parar de ler este artigo. Se o vazamento de objetos wrapper não foi detectado, seu código usa a API GDI diretamente e há um cenário em que eles não são excluídos

O que os outros recomendam?


A orientação oficial da Microsoft ou outros artigos sobre esse assunto recomendarão algo assim:

Encontre todos os Criar %SOME_GDI_OBJECT% e detectar se o DeleteObject correspondente (ou ReleaseDC para objetos HDC) existe. Se tal ExcluirObjeto existe, pode haver um cenário que não o chama.

Existe uma versão ligeiramente melhorada deste método que contém um passo adicional:

Baixe o utilitário GDIView. Ele pode mostrar o número exato de objetos GDI por tipo. Observe que o número total de objetos não corresponde ao valor da última coluna. Mas podemos fechar os olhos sobre isso se isso ajudar a restringir o campo de pesquisa.



O projeto em que estou trabalhando tem a base de código de 9 milhões de registros, aproximadamente a mesma quantidade de registros está localizada nas bibliotecas de terceiros, centenas de chamadas da função GDI que estão espalhadas por dezenas de arquivos. Perdi muito tempo e energia antes de entender que a análise manual sem falhas é impossível.

O que posso oferecer?


Se esse método parece muito longo e cansativo para você, você não passou por todos os estágios de desespero com o anterior. Você pode tentar seguir as etapas anteriores, mas se isso não ajudar, não se esqueça desta solução.

Em busca do vazamento, me questionei:Onde são criados os objetos que vazam? Era impossível definir pontos de interrupção em todos os locais onde a função da API é chamada. Além disso, eu não tinha certeza de que isso não acontecesse no .NET Framework ou em uma das bibliotecas de terceiros que usamos. Poucos minutos de pesquisa no Google me levaram ao utilitário API Monitor que permitia registrar e rastrear chamadas para todas as funções do sistema. Encontrei facilmente a lista de todas as funções que geram objetos GDI, localizei e selecionei-as no API Monitor. Então, eu defino pontos de interrupção.



Depois disso, executei o processo de depuração em Visual Studio e selecione-o na árvore Processos. O quinto ponto de interrupção funcionou imediatamente:



Percebi que me afogaria nessa torrente e que precisava de outra coisa. Apaguei os breakpoints das funções e decidi ver o log. Mostrou milhares de chamadas. Ficou claro que não poderei analisá-los manualmente.



A tarefa é Encontrar as chamadas das funções GDI que não causam a exclusão . O log continha tudo o que eu precisava:a lista de chamadas de função em ordem cronológica, seus valores retornados e parâmetros. Portanto, eu precisava obter um valor retornado da função Create%SOME_GDI_OBJECT% e encontrar a chamada de DeleteObject com esse valor como argumento. Selecionei todos os registros no API Monitor, inseri-os em um arquivo de texto e obtive algo como CSV com o delimitador TAB. Executei o VS, onde pretendia escrever um pequeno programa para análise, mas antes que pudesse carregar, uma ideia melhor me veio à mente:exportar dados para um banco de dados e escrever uma consulta para encontrar o que preciso. Foi a escolha certa, pois me permitiu fazer perguntas e obter respostas rapidamente.

Existem muitas ferramentas para importar dados de CSV para um banco de dados, então não vou me alongar neste assunto (mysql, mssql, sqlite).

Tenho a seguinte tabela:
CREATE TABLE apicalls (
id int(11) DEFAULT NULL,
`Time of Day` datetime DEFAULT NULL,
Thread int(11) DEFAULT NULL,
Module varchar(50) DEFAULT NULL,
API varchar(200) DEFAULT NULL,
`Return Value` varchar(50) DEFAULT NULL,
Error varchar(100) DEFAULT NULL,
Duration varchar(50) DEFAULT NULL
)

Eu escrevi a seguinte função MySQL para obter o descritor do objeto excluído da chamada da API:
CREATE FUNCTION getHandle(api varchar(1000))
RETURNS varchar(100) CHARSET utf8
BEGIN
DECLARE start int(11);
DECLARE result varchar(100);
SET start := INSTR(api,','); -- for ReleaseDC where HDC is second parameter. ex: 'ReleaseDC ( 0x0000000000010010, 0xffffffffd0010edf )'
IF start = 0 THEN
SET start := INSTR(api, '(');
END IF;
SET result := SUBSTRING_INDEX(SUBSTR(api, start + 1), ')', 1);
RETURN TRIM(result);
END

E, finalmente, escrevi uma consulta para localizar todos os objetos atuais:
SELECT creates.id, creates.handle chandle, creates.API, dels.API deletedApi
FROM (SELECT a.id, a.`Return Value` handle, a.API FROM apicalls a WHERE a.API LIKE 'Create%') creates
LEFT JOIN (SELECT
d.id,
d.API,
getHandle(d.API) handle
FROM apicalls d
WHERE API LIKE 'DeleteObject%'
OR API LIKE 'ReleaseDC%' LIMIT 0, 100) dels
ON dels.handle = creates.handle
WHERE creates.API LIKE 'Create%';

(Basicamente, ele simplesmente encontrará todas as chamadas Excluir para todas as chamadas Criar).



Como você vê na imagem acima, todas as chamadas sem um único Excluir foram encontradas de uma só vez.

Então, a última pergunta foi deixada:Como determinar, de onde esses métodos são chamados no contexto do meu código? E aqui um truque chique me ajudou:
  1. Execute o aplicativo no VS para depuração
  2. Encontre-o no Api Monitor e selecione-o.
  3. Selecione uma função obrigatória na API e coloque um ponto de interrupção.
  4. Continue clicando em 'Avançar' até que ele seja chamado com os parâmetros em questão (eu realmente perdi pontos de interrupção condicionais do VS)
  5. Quando você chegar à chamada necessária, mude para o CS e clique em Quebrar tudo .
  6. O VS Debugger será interrompido exatamente onde o objeto com vazamento é criado e tudo o que você precisa fazer é descobrir por que ele não foi excluído.

Nota:O código foi escrito para fins de ilustração.

Resumo:


O algoritmo descrito é complicado e requer muitas ferramentas, mas deu o resultado muito mais rápido em comparação com uma pesquisa burra através da enorme base de código.

Aqui está um resumo de todas as etapas:
  1. Procure vazamentos de memória de objetos wrapper GDI.
  2. Se existirem, elimine-os e repita a etapa 1.
  3. Se não houver vazamentos, pesquise explicitamente as chamadas para as funções da API.
  4. Se a quantidade deles não for grande, procure um script em que um objeto não seja excluído.
  5. Se a quantidade deles for grande ou dificilmente puderem ser rastreados, baixe o API Monitor e configure-o para registrar chamadas das funções GDI.
  6. Execute o aplicativo para depuração no VS.
  7. Reproduza o vazamento (ele inicializará o programa para ocultar os objetos descontados).
  8. Conecte-se com o Monitor de API.
  9. Reproduza o vazamento.
  10. Copie o log em um arquivo de texto, importe-o para qualquer banco de dados disponível (os scripts apresentados neste artigo são para MySQL, mas podem ser facilmente adotados para qualquer sistema de gerenciamento de banco de dados relacional).
  11. Compare os métodos Create e Delete (você pode encontrar o script SQL neste artigo acima) e encontre os métodos sem as chamadas Delete.
  12. Defina um ponto de interrupção no API Monitor na chamada do método necessário.
  13. Continue clicando em Continuar até que o método seja chamado com parâmetros readquiridos.
  14. Quando o método for chamado com os parâmetros obrigatórios, clique em Break All in VS.
  15. Descubra por que este objeto não foi excluído.

Espero que este artigo seja útil e ajude você a economizar seu tempo.