Monitoreo de recolección de basura en C #

Tengo una aplicación WPF que está experimentando muchos problemas de rendimiento. Lo peor de ellos es que a veces la aplicación simplemente se congela por unos segundos antes de volver a funcionar.

Actualmente estoy depurando la aplicación para ver a qué se relaciona este congelamiento, y creo que una de las cosas que puede estar causando es el colector de basura. Como mi aplicación se ejecuta en un entorno muy limitado, creo que el recolector de basura puede usar todos los recursos de la máquina cuando se ejecuta y no dejar nada para nuestra aplicación.

Para verificar esta hipótesis, encontré estos artículos: Notificaciones de recolección de basura y Notificaciones de recolección de basura en .NET 4.0 , que explican cómo se puede notificar a mi aplicación cuándo comenzará a ejecutarse el Recolector de basura y cuándo terminará.

Entonces, basado en esos artículos, creé la clase a continuación para recibir las notificaciones:

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(); } } 

Y agregué la opción gcConcurrent a mi archivo app.config (a continuación):

    

Sin embargo, cada vez que se ejecuta la aplicación, parece que no se envía ninguna notificación de que se ejecutará el Recolector de basura. Puse puntos de interrupción en DoGCMonitoring y parece que las condiciones (s == GCNotificationStatus.Succeeded) y (s == GCNotificationStatus.Succeeded) nunca se cumplen, por lo tanto, el contenido de esas declaraciones ifs nunca se ejecuta.

¿Qué estoy haciendo mal?

Nota: Estoy usando C # con WPF y .NET Framework 3.5.

ACTUALIZACIÓN 1

Actualicé mi prueba GCMonitor con el método AllocationTest. Este método es solo para fines de prueba. Solo quería asegurarme de que se estaba asignando suficiente memoria para forzar al Garbage Collector a ejecutarse.

ACTUALIZACIÓN 2

Se actualizó el método DoGCMonitoring, con nuevas comprobaciones sobre la devolución de los métodos WaitForFullGCApproach y WaitForFullGCComplete. Por lo que he visto hasta ahora, mi aplicación va directamente a la condición (s == GCNotificationStatus.NotApplicable). Así que creo que tengo una mala configuración en alguna parte que me impide obtener los resultados deseados.

La documentación para GCNotificationStatus enum se puede encontrar aquí .

No veo GC.RegisterForFullGCNotification(int,int) en ninguna parte de su código. Parece que está utilizando los WaitForFullGC[xxx] , pero nunca se está registrando para la notificación. Esa es probablemente la razón por la que obtiene el estado NotApplicable.

Sin embargo, dudo que el GC sea su problema, mientras sea posible, supongo que sería bueno conocer todos los modos de GC que existen y las mejores formas de determinar lo que está sucediendo. Hay dos modos de recolección de basura en .NET: el servidor y la estación de trabajo. Ambos recostackn la misma memoria no utilizada, sin embargo, la forma en que se hace es ligeramente diferente.

  • Versión del servidor : este modo le dice al GC que está utilizando una aplicación del lado del servidor e intenta optimizar las colecciones para estos escenarios. Dividirá el montón en varias secciones, 1 por CPU. Cuando se inicia el GC, se ejecutará un hilo en cada CPU en paralelo. Realmente quieres CPUs múltiples para que esto funcione bien. Si bien la versión del servidor usa múltiples subprocesos para el GC, no es lo mismo que el modo GC de la estación de trabajo concurrente que se detalla a continuación. Cada hilo actúa como la versión no simultánea.

  • Versión de estación de trabajo : este modo le dice a GC que está usando una aplicación del lado del cliente. Se da cuenta de que tiene recursos más limitados que la versión de Servidor, por lo que solo hay un hilo de GC. Sin embargo, hay dos configuraciones de la versión de la estación de trabajo: concurrente y no concurrente.

    • Concurrente : esta es la versión activada por defecto cada vez que se utiliza la estación de trabajo GC (este sería el caso de su aplicación WPF). El GC siempre se ejecuta en un subproceso separado que siempre marca los objetos para la recostackción cuando la aplicación se está ejecutando. Además, elige si compactar o no la memoria en ciertas generaciones y toma esa decisión en función del rendimiento. Todavía tiene que congelar todos los hilos para ejecutar una colección si se realiza la compactación, pero casi nunca verá una aplicación que no responde al usar este modo. Esto crea una mejor experiencia interactiva para los usos y es mejor para aplicaciones de consola o GUI.
    • No simultáneo : esta es una versión que puede configurar su aplicación para usar, si lo desea. En este modo, el hilo del GC duerme hasta que se inicia un GC, luego marca y marca todos los árboles de objetos que son basura, libera la memoria y la compacta, mientras todos los demás hilos quedan suspendidos. Esto puede causar que la aplicación deje de responder por un corto período de tiempo.

No puede registrarse para recibir notificaciones en el recostackdor simultáneo, ya que eso se hace en segundo plano. Es posible que su aplicación no esté utilizando el recostackdor simultáneo (me percaté de que tiene el gcConcurrent deshabilitado en el app.config , pero parece que solo es para probarlo). Si ese es el caso, ciertamente puede ver congelar su aplicación si hay colecciones pesadas. Es por eso que crearon el coleccionista concurrente. El tipo de modo GC puede configurarse parcialmente en código y configurarse por completo en configuraciones de aplicaciones y configuraciones de máquina.

¿Qué podemos hacer para descubrir exactamente qué está usando nuestra aplicación? En tiempo de ejecución, puede consultar la clase GCSettings estática (en System.Runtime ). GCSettings.IsServerGC le dirá si está ejecutando la estación de trabajo en las versiones del servidor y GCSettings.LatencyMode puede decirle si está utilizando el concurrente, no concurrente o uno especial que tiene que establecer en el código que no es realmente aplicable aquí. Creo que sería un buen lugar para comenzar, y podría explicar por qué funciona bien en su máquina, pero no en la producción.

En los archivos de configuración, o controlan los modos del recolector de basura. Tenga en cuenta que esto puede estar en su archivo app.config (ubicado a un lado del ensamblaje en ejecución) o en el archivo machine.config, que se encuentra en %windir%\Microsoft.NET\Framework\[version]\CONFIG\

También puede usar remotamente el Monitor de rendimiento de Windows para acceder a los contadores de rendimiento de la máquina de producción para la recolección de basura .NET y ver esas estadísticas. Puede hacer lo mismo con Event Tracing para Windows (ETW) de forma remota. Para el monitor de rendimiento, querrá el objeto .NET CLR Memory , y seleccione su aplicación en el cuadro de lista instancias.