Monitorando o Coletor de Lixo em C #

Eu tenho um aplicativo WPF que está enfrentando muitos problemas de desempenho. O pior deles é que às vezes o aplicativo congela por alguns segundos antes de voltar a funcionar.

Atualmente estou depurando o aplicativo para ver o que este congelamento pode estar relacionado, e acredito que uma das coisas que podem estar causando isso é o Garbage Collector. Como meu aplicativo está sendo executado em um ambiente muito limitado, acredito que o Garbage Collector pode estar usando todos os resources da máquina quando ela é executada e não deixa nenhum em nosso aplicativo.

Para verificar essas hipóteses, encontrei esses artigos: Notificações de garbage collection e Notificações de garbage collection no .NET 4.0 , que explicam como meu aplicativo pode ser notificado quando o Coletor de lixo começa a ser executado e quando é concluído.

Então, com base nesses artigos, criei a class abaixo para receber as notifications:

public sealed class GCMonitor { private static volatile GCMonitor instance; private static object syncRoot = new object(); private Thread gcMonitorThread; private ThreadStart gcMonitorThreadStart; private bool isRunning; public static GCMonitor GetInstance() { if (instance == null) { lock (syncRoot) { instance = new GCMonitor(); } } return instance; } private GCMonitor() { isRunning = false; gcMonitorThreadStart = new ThreadStart(DoGCMonitoring); gcMonitorThread = new Thread(gcMonitorThreadStart); } public void StartGCMonitoring() { if (!isRunning) { gcMonitorThread.Start(); isRunning = true; AllocationTest(); } } private void DoGCMonitoring() { long beforeGC = 0; long afterGC = 0; try { while (true) { // Check for a notification of an approaching collection. GCNotificationStatus s = GC.WaitForFullGCApproach(10000); if (s == GCNotificationStatus.Succeeded) { //Call event beforeGC = GC.GetTotalMemory(false); LogHelper.Log.InfoFormat("===> GC  GC  GC  GC  GC  GC  0) { LogHelper.Log.InfoFormat("===> GC  GC  GC  GC  GC  { while (true) { List lst = new List(); try { for (int i = 0; i <= 30; i++) { char[] bbb = new char[900000]; // creates a block of 1000 characters lst.Add(bbb); // Adding to list ensures that the object doesnt gets out of scope } Thread.Sleep(1000); } catch (Exception ex) { LogHelper.Log.Error(" ******************** Garbage Collector Error ************************ "); LogHelper.LogAllErrorExceptions(e); LogHelper.Log.Error(" ------------------- Garbage Collector Error --------------------- "); } } }); stress.Start(); } } 

E eu adicionei a opção gcConcurrent ao meu arquivo app.config (abaixo):

    

No entanto, sempre que o aplicativo é executado, parece que nenhuma notificação é enviada que o Coletor de lixo será executado. Eu coloquei pontos de interrupção no DoGCMonitoring e parece que as condições (s == GCNotificationStatus.Succeeded) e (s == GCNotificationStatus.Succeeded) nunca são satisfeitas, portanto, o conteúdo dessas instruções ifs nunca é executado.

O que estou fazendo de errado?

Nota: Estou usando o C # com o WPF e o .NET Framework 3.5.

ATUALIZAÇÃO 1

Atualizei meu teste do GCMonitor com o método AllocationTest. Este método é apenas para fins de teste. Eu só queria ter certeza de que memory suficiente estava sendo alocada para forçar o Garbage Collector a rodar.

ATUALIZAÇÃO 2

Atualizado o método DoGCMonitoring, com novas verificações no retorno dos methods WaitForFullGCApproach e WaitForFullGCComplete. Pelo que vi até agora, meu aplicativo está indo diretamente para a condição (s == GCNotificationStatus.NotApplicable). Então eu acho que tenho algum erro de configuração em algum lugar que está me impedindo de obter os resultados desejados.

A documentação para o enum GCNotificationStatus pode ser encontrada aqui .

Eu não vejo GC.RegisterForFullGCNotification(int,int) em qualquer parte do seu código. Parece que você está usando os WaitForFullGC[xxx] , mas nunca está se registrando para a notificação. É provavelmente por isso que você está recebendo o status NotApplicable.

No entanto, estou duvidando que o GC seja o seu problema, embora seja possível, suponho que seria bom saber sobre todos os modos de GC existentes e as melhores maneiras de determinar o que está acontecendo. Existem dois modos de garbage collection no .NET: o servidor e a estação de trabalho. Ambos coletam a mesma memory não usada, no entanto, a maneira como é feita é sempre ligeiramente diferente.

  • Versão do servidor – Esse modo informa ao GC que você está usando um aplicativo do lado do servidor e tenta otimizar as collections para esses cenários. Ele dividirá o heap em várias seções, 1 por CPU. Quando o GC é iniciado, ele executará um thread em cada CPU em paralelo. Você realmente quer várias CPUs para que isso funcione bem. Embora a versão do servidor use vários encadeamentos para o GC, não é o mesmo que o modo GC da estação de trabalho simultânea listado abaixo. Cada thread age como a versão não concorrente.

  • Versão da estação de trabalho – Este modo informa ao GC que você está usando um aplicativo do lado do cliente. Isso significa que você tem resources mais limitados do que a versão do servidor e, portanto, há apenas um thread do GC. No entanto, existem duas configurações da versão da estação de trabalho: concorrente e não concorrente.

    • Concorrente – Esta é a versão ativada por padrão sempre que o GC da estação de trabalho é usado (esse seria o caso do seu aplicativo WPF). O GC está sempre sendo executado em um thread separado que está sempre marcando objects para coleta quando o aplicativo está em execução. Além disso, ele escolhe compactar ou não a memory em certas gerações e faz essa escolha com base no desempenho. Ele ainda tem que congelar todos os threads para executar uma coleção se a compactação estiver concluída, mas você quase nunca verá um aplicativo que não esteja respondendo ao usar esse modo. Isso cria uma experiência interativa melhor para usos e é melhor para aplicativos de console ou GUI.
    • Não Concorrente – Esta é uma versão que você pode configurar para usar seu aplicativo, se desejar. Nesse modo, o encadeamento do GC fica inativo até que um GC seja iniciado, então ele marca e marca todas as trees de objects que são lixo, libera a memory e a compacta, enquanto todos os outros encadeamentos são suspensos. Isso pode fazer com que o aplicativo às vezes pare de responder por um curto período de tempo.

Você não pode se registrar para notifications no coletor concorrente, pois isso é feito em segundo plano. É possível que seu aplicativo não esteja usando o coletor concorrente (percebo que você tem o gcConcurrent desativado no app.config , mas parece que é apenas para testes?). Se for esse o caso, você certamente verá o congelamento de seu aplicativo se houver collections pesadas. É por isso que eles criaram o coletor concorrente. O tipo de modo GC pode ser parcialmente definido no código e totalmente definido nas configurações do aplicativo e nas configurações da máquina.

O que podemos fazer para descobrir exatamente o que nosso aplicativo está usando? Em tempo de execução, você pode consultar a class estática GCSettings (em System.Runtime ). GCSettings.IsServerGC informará se você está executando a estação de trabalho nas versões do servidor e o GCSettings.LatencyMode pode informar se você está usando o concorrente, não simultâneo ou especial que você precisa definir no código que não é realmente aplicável Aqui. Acho que seria um bom começo e poderia explicar por que está funcionando bem em sua máquina, mas não em produção.

Nos arquivos de configuração, ou controla os modos do coletor de lixo. Lembre-se de que isso pode estar no arquivo app.config (localizado ao lado do assembly em execução) ou no arquivo machine.config, localizado em %windir%\Microsoft.NET\Framework\[version]\CONFIG\

Você também pode usar remotamente o Windows Performance Monitor para acessar os contadores de desempenho da máquina de produção para garbage collection do .NET e exibir essas statistics. Você pode fazer o mesmo com o Rastreamento de Eventos para Windows (ETW) remotamente. Para o monitor de desempenho, você deseja o object .NET CLR Memory e selecione seu aplicativo na checkbox de listview de instâncias.