Surviving the Night

El blog de Pablo Doval sobre .NET, SQL, WinDbg...

Children of the Damned: Cuidado con vuestras aplicaciones MDI

A pesar del titulo de esta entrada, no voy a entrar a discutir si las aplicaciones con interfaz MDI son usables o no, si son feas como el demonio o si le dan un bonito toque retro a nuestras aplicaciones. No, eso se lo dejo a nuestros chicos de UX.

Yo me pondré el uniforme del DOT, ya que os voy a hablar de un escenario que nos encontramos recientemente en un cliente nuestro y que reunió, en único caso, los tres ingredientes que toda buena sesión de depuración y optimización deben de tener: problemas de gestión de memoria, un bug escandaloso y grandes dosis de cafeína :)

Al turrón…

El Problema

Nuestro cliente requirió nuestros servicios de optimización para optimizar una aplicación que iba a pasar a producción en menos de un mes. Como os podéis imaginar éste requisito supone todo un reto, ya que nos podemos ir olvidando de las mejores recomendaciones a nivel de rendimiento – las que implican cambios de arquitectura – y nos obliga a centrarnos en la búsqueda de rendimiento línea a línea, byte a byte. Divertidísimo, seguro, pero no tanto cuando solo tienes una semana, ¡y las máquinas cliente que van a ejecutar la aplicación cuentan con 512Mb en el mejor de los casos!

Bien, con estas premisas os podéis imaginar que uno de nuestros objetivos principales fue estudiar el consumo de memoria de la aplicación, monitorizando los aspectos básicos como el número de recolecciones de basura en cada generación, la memoria total en el Managed Heap, y la valiosísima información mostrada por el comando !DumpHeap –stat de las SOS.dll para comprobar el histograma de distribución de Tipos en el Managed Heap. También agregamos a nuestro arsenal el .NET Memory Profiler, un producto que, a pesar de ser poco más que un maquillaje sobre el venerable CLR Profiler en muchos aspectos, me ha sorprendido muy gratamente por algunas de sus funcionalidades, su facilidad de uso y de lectura de datos. En breves espero hacer una pequeña reseña del mismo, pero os invito a que os descarguéis la versión de evaluación y empecéis a jugar con ella.

Nuestras primeras investigaciones arrojaron unos números elevadísimos de recolecciones de basura, mucha fragmentación de memoria y, en definitiva, una gran cantidad de indicaciones de que la aplicación estaba sufriendo una gran presión de memoria. Modificamos la aplicación levemente para agregarle un botón que invocaba a GC.Collect() y GC.WaitForPendingFinalizers(), y lanzamos una serie de pruebas monitorizando como se comportaba la memoria y si estábamos fugando en algún sitio.

… y descubrimos algo interesante ;) La interfaz de la aplicación consistía en una ventana MDI padre, que instanciaba una ventana MDI hija para cada tipo de operativa de la aplicación. Estas ventanas tenían un coste masivo en memoria, pues contenían gran cantidad de DataSets, DataTables, etc. pudiendo alcanzar los 60Mb en memoria, pero el cliente confiaba en que al cerrar la ventana esta memoria se eliminara. Hicimos una prueba muy sencilla, por el método clásico ;)

  1. Abrimos la aplicación y comprobamos el estado de la memoria en vacio.
  2. Abrimos un formulario, trabajamos un poquito con el.
  3. Cerramos el formulario.
  4. Pulsamos el botón mágico que invoca al GC y monitorizamos el estado de la memoria.

Al fin de la prueba pudimos comprobar como la memoria consumida por el formulario no se liberaba nunca, a pesar de las invocaciones explicitas al GC.Collect(). Estaba claro que había alguna referencia al formulario hijo que no se liberaba, y eso impedía la recolección del mismo, con todos sus instancias internas de DataTables, etc. Como ya hemos visto en otros casos en este blog, tiramos de !DumpHeap para encontrar la dirección del formulario en cuestión (MDIPropuesta) y, a partir de su dirección, miramos las roots con !GCRoot:

0:011> !DumpHeap -stat
total 59745 objects
Statistics:
      MT    Count    TotalSize Class Name
[…]
03c94a3c        1          496 XXX.Dominio.Activo.Propuestas.IU.Formularios.MDIPropuesta
[…]

0:011> !DumpHeap -mt 03c94a3c
Address       MT     Size
01b23528 03c94a3c      496    
total 1 objects
Statistics:
      MT    Count    TotalSize Class Name
03c94a3c        1          496 XXX.Dominio.Activo.Propuestas.IU.Formularios.MDIPropuesta
Total 1 objects

0:011> !GCRoot 01b23528
eax:Root:01555428(System.Windows.Forms.Application+ComponentManager+ComponentHashtableEntry)->
015247f8(System.Windows.Forms.Application+ThreadContext)->
015d3910(XXX.Dominio.Activo.Propuestas.IU.Formularios.ShellMDI)->
015d3b08(System.Windows.Forms.PropertyStore)->
01632b94(System.Windows.Forms.PropertyStore+ObjectEntry[])

Como podemos ver, tenemos una referencia en el PropertyStore de la clase ShellMDI, la ventana MDI principal. Por alguna razón, no esta soltando su referencia a la ventana hija al salir esta de contexto, y en esta aplicación esta provocando que entre 40 y 60 preciados Mb no se liberen, provocando gran cantidad de GCs efímeras y la promoción de objetos de corta vida a generación 2 en muchos escenarios.

La Solución

Buscando un poco por la red pudimos comprobar que se trata de un problema conocido y reportado en https://connect.microsoft.com/VisualStudio/feedback/ViewFeedback.aspx?FeedbackID=357705&wa=wsignin1.0. Actualmente no hay parche oficial de Microsoft que conozcamos, pero aprovechándome de la información recogida en otros foros y documentos, os resumo el problema y la solución.

Hasta la versión 2.0 RTM del framework .NET, aparecía el siguiente fragmento de código para actualizar el icono cuando una ventana hija MDI es desactivada:

if( this.FormerlyActiveMdiChild != null )
{
    this.FormerlyActiveMdiChild.UpdateWindowIcon(true);
    this.FormerlyActiveMdiChild = null;
}

En la versión 2.0 SP1 y siguientes, Microsoft modifico el código para resolver un problema en el caso de que la ventana se estuviera cerrando en ese momento:

if( this.FormerlyActiveMdiChild != null &&
    !this.FormerlyActiveMdiChild.IsClosing )
{
this.FormerlyActiveMdiChild.UpdateWindowIcon(true);
    this.FormerlyActiveMdiChild = null;
}

Este cambio en el código provoca que, en ese escenario, no se ponga a NULL la propiedad FormerlyActiveMDIChild, manteniendo el padre siempre una referencia a su ultima ventana hija activa, provocando la fuga de memoria comentada.

Como solución, adoptamos una publicada orginalmente en el blog .NET and Memory, que consiste en poner la propiedad FormerlyActiveMdiChild a NULL a manopla mediante reflexión en el evento OnMdiChildActivate:

protected override void OnMdiChildActivate( EventArgs e )
{
  base.OnMdiChildActivate( e );
  try
  {
    typeof( Form ).InvokeMember( "FormerlyActiveMdiChild",
      BindingFlags.Instance | BindingFlags.SetProperty |
      BindingFlags.NonPublic, null,
      this, new object[] { null } );
  }
  catch( Exception )
  {
  }
}

Una vez implementado este cambio, comprobamos como la aplicación comenzó a comportarse como se esperaba, liberando la memoria en las siguientes invocaciones al GC.Collect() posteriores al cierre del formulario.

En nuestro caso particular, persistieron otros problemas de presión de memoria… pero como ya decía el cronista de Conan, eso, amigos… eso es otra historia ;)

Conclusión

Seré breve: Si vuestras aplicaciones emplean el framework .NET 2.0 SP1 o superior y estáis empleando una interfaz MDI, con toda probabilidad seréis victimas de este problema. No hay fix oficial de Microsoft, así que os recomiendo el workaround sugerido.

No es una perdida de memoria acumulativa, solo perderéis la memoria del ultimo formulario abierto, pero aún así puede ser determinante como en el caso que os he expuesto aquí.

Notas Finales

  1. Supongo que a estas alturas no es necesario recordarlo, pero nunca esta de más: las continuas referencias en este articulo a GC.Collect() se han empleado solo con fines de depuración y análisis. Nunca debéis invocar explícitamente al GC a no ser que Jeffrey Richter, Maoni o Luis Guerrero os lo pidan en persona :)
  2. Cada vez que invocáis a GC.Collect(), Dios mata a un gatito.
  3. De verdad, no lo hagáis.

Rock Tip:

Children of the Daaaaaaaaamneed!!! Chiiiiildren of the Damneeeeeeed!! (entónese con estilo Bruce Dickinson o estilo alcade Adam West, es indiferente).

La aventurilla de los formularios ‘hijos’ de ese ‘maldito’ MDIParent me sirven como excusa perfecta para recomendaros ese ‘Children of the Damned’, temazo aparecido en el The Number of the Beast de los Iron Maiden alla por el año 1982.

Por cierto, esta canción la empleaba Sebastian Bach en sus tiempos de Skid Row para calentar y para las pruebas de sonido; ¡no os perdáis sus excelentes versiónes!.

Posted: 2/2/2009 18:25 por Pablo Alvarez | con 24 comment(s) |
Archivado en: ,,
Comparte este post:

Comentarios

Paco ha opinado:

Es uno de los casos más cañeros que he visto de fugas de memoria... lo que más mola es la solución... El extraño caso de Reflection contra las fugas de memoria se podría llamar.

Da gusto leer tu post explicandolo. Ameno y didáctico...

Ya espero con impaciencia la siguiente aventura del DOT ejejejej...

# February 3, 2009 12:20 PM

Octavio Hernández ha opinado:

Pablito,

Excelente post, como siempre.

Aunque tú te estabas refiriendo a otro Conan, el Conan que me vino a la mente al leer el post fue Conan Doyle, pues vuestras investigaciones son dignas del maestro Sherlock Holmes :-)

Abrazo - Octavio

# February 3, 2009 4:36 PM

Pablo Alvarez ha opinado:

¡Gracias a los dos! :)

@Octavio: Si no me equivoco, creo que tu fuiste la primera persona que me hablo del .NET Memory Profiler! La verdad es que esta muy chulo, gracias maestro! :)

Abrazotes a los dos!

# February 3, 2009 4:40 PM

Octavio Hernández ha opinado:

Sí, yo lo estoy usando desde hace algún tiempo y encantado.

Cierto amigo me dijo (no entendí si de broma o en serio) que eso de las herramientas era para "gente floja", que los verdaderos "cowboys" se las debían arreglar con WinDbg y el CLR Profiler, pero como yo ya estoy un poco mayor, me fui por la vía fácil :-).

# February 3, 2009 4:55 PM

Pablo Alvarez ha opinado:

Estoy seguro de que era medio en broma y medio en serio :) Es cierto que estas herramientas nos facilitan mucho, pero nunca me daran las cosas que tanto me gustan del WinDbg, como mi bien amado .foreach...

.foreach (obj {!DumpHeap -mt 79104c38 -short}) { !GCRoot ${obj} } <-- habra algo mas lindo que esto en la depuracion? XD

Un abrazote!

# February 3, 2009 5:03 PM

Octavio Hernández ha opinado:

Ah! Qué bueno eso!

Abrazo - Octavio

# February 3, 2009 5:07 PM

Eduard Tomàs i Avellana ha opinado:

MMmmm....

@PabloNetrix

Quizá... pero eso le ocurre al cliente en el que estoy ahora, y si tiene que cambiar más de 2000 máquinas... ya no creo que le salga rentable ;-) (o quzás sí, no me se las tarifas del DOT Team :p)

Me refiero a que como todo... es cuestión de mesura :)

Saludos!

pd: Excelente post

# February 3, 2009 6:34 PM

Iván González ha opinado:

Creo que las cifras podrían ser algo así:

1.000 PCs * 30€ de módulos de memoria para cada PC = 30.000€

Más mandar un técnico a aprox. 100 oficinas a cambiarla: ni idea pero un dinero.

VS

3 días de trabajo del DOT donde además de esto se han arreglado más cosas en la aplicación.

Creo que salimos muuuucho más baratos

:-)

# February 3, 2009 6:53 PM

Rodrigo Corral ha opinado:

Jjajajaja... Iván, después de leer tu comentario creo que tenemos que subir las tarifas del Debugging & Optimization Team de Plain Concepts... ¡salimos muy baratos! :P

Creo que PabloNetrix desconocía la realidad del proyecto. Es cierto que a veces el hardware soluciona el problema.

Pero también es cierto que en este caso, a parte de la evidente mejora de rendimiento, el problema es que no hay cantidad de memoria que aguante una fuga continua... Así que esto nisiquiera se arreglaba con 30 euros de memoria por PC.

Generalmente cuando la gente llega al DOT, ya ha quemado muchas naves antes, el mejorar el hardware la primera, si es que es posible, a menudo sin los resultados esperados.

Además, hay otra cuestión, PabloNetrix, llamar al DOT es gratis... si no mejoramos la situación y logramos que el cliente quede satisfecho ¡no cobramos!... La conclusión es clara: Antes de comprar hardware, llama a los chicos del DOT, quizás no te ahorres el tener que mejorar tu hardware, a veces no queda otra, pero seguro que podrás comprar hardware más modesto :)

¡Un saludo!

# February 3, 2009 8:17 PM

PabloNetrix ha opinado:

Jeje, no, a ver, está claro que si hablamos de parques de centenares de PC's la cosa puede cambiar (y "con la que está cayendo" pues como para no coger un proyecto de optimización así).

Y sí, supongo que quien ha decidido llamar al DOT es alguien que, primero, SABE que existen los DOT ;) y evidentemente habrá tenido en consideración todos los factores.

Un saludo a todos!

# February 3, 2009 8:33 PM

Pablo Alvarez ha opinado:

Ay dios, marcho un ratin (precisamente a apagar un fuego en un cliente XD) y la que me liais en el blog.. xD

No voy a añadir mucho mas a lo ya dicho, pero si que me vais a permitir que os cuente una pequeña batallita:

Hace unos años, cuando aun trabajaba en Microsoft, un cliente MUY grande nos requirió por una Severidad A (un caso critico). Desde el punto de vista de la base de datos yo identifiqué en menos de una hora, y con pruebas tangibles a través de un volcado, que el cliente estaba cayendo en un bug del producto, y que se solucionaría actualizando de SQL Server 2000 SP3a a SP4; que por otro lado era el único nivel de soporte de SQL Server por aquel momento.

El cliente se negaba en redondo a actualizar a un Service Pack, incluso pese a la recomendación del equipo de soporte de Microsoft y las pruebas palpables, así que llamo a IBM para 'echar mas hierro'... ya sabis, la tipica llamada de 'Mas potencia, Scotty!!'.

No hace falta que os diga que al dia siguiente, nos volvieron a llamar, porque el sistema volvio a caer con el pico de usuarios. Nuevamente dimos la misma recomendacion, y nuevamente estimaron mas barato y rapido meter aún mas hierro. Al día siguiente, ¿sabeis lo que paso?

Estuvimos así cinco días, de Lunes a Viernes, para que al final instalaran el service pack y, de paso, quitaran hierro que ya no necesitaban.

Con esto no pretendo decir que este mal actualizar nuestras máquinas, pero hay que tener en cuenta que:

- No siempre es posible

- No siempre resuelve los problemas de rendimiento o estabilidad

- No debemos ver el aumentar el Hardware como una enorme alfombra donde esconder nuestra mierda (con perdon)

En otro orden de cosas, me encanta que os haya interesado el tema :)

# February 3, 2009 11:49 PM

Pablo Alvarez ha opinado:

@espinete:

 Muchas gracias a ti por leerlos y apreciarlos!! :)

@novatito:

 Antes de nada, agradecerte los comentarios, pero evidentmente de superiores no tenemos nada! Bueno, como mucho Rodri, pero porque el mide 2 metros y esta fuertecito! XD

"Presion de memoria" es el termino que utilizamos para indicar que se estan creando nuevos objetos y que eso hace que se consuma memoria dentro del Managed Heap. Recuerda siempre que cuando haces una reserva de memoria en .NET, esta se realiza en un lugar especial de la memoria llamado el managed heap, que es el ámbito donde actuará el recolector de basura. Decimos "presión de memoria" y no simplemente que estamos consumiendo memoria, para ser conscientes de que ciertamente esa memoria 'perdida' no es problematica en si misma (ya pasará el recolector a liberar la memoria que no este seindo usada), pero que puede haber problemas por que al 'incrementar presion' en el managed heap se fuerze un numero mayor de recolecciones de basura, lo que puede afectar severamente al rendimiento.

La verdad es que llevo mucho tiempo con la idea de hacer un par de articulos sobre la gestión de memoria en .NET.. y ahora que estan de moda las novedades del GC enla versión 4.0 creo que es un buen momento, así que intentaré hacer en breve un par de articulos que expliquen esto en mayor profundidad.

Respecto al .NET memory profiler, pues lo cierto es que estoy pendiente también de hacer ese articulo. Tomo nota de tu sugerencia.

Un saludo a los dos!

# February 4, 2009 12:18 PM

Luis Guerrero ha opinado:

Pablo enhorabuena por el post es excelente!!, en cuanto a al tema que nos atañe, yo estuve con  Pablo en este caso del DOT y la verdad es que fue un verdadero reto para nosotros. Estoy de acuerdo en todo lo que ha dicho Pablo porque lo ha descrito con total exactitud. En cuanto al .NET Memory Profiler a ver si me animo y escribo algún artículo curioso también.

Saludos desde Seattle, que está de pm. Luis.

# February 5, 2009 4:31 AM

Fran Peula Ariza ha opinado:

Pableras, eres el Chuck Norris de la optimización; Contigo la memoria no se fuga, ¡¡espera asustada a que tú la liberes!!

¡¡Enhorabuena y gracias por habernos enseñado y seguir enseñándonos tanto!!

# February 5, 2009 10:19 AM

Pablo Alvarez ha opinado:

@Luis: ¡¡Hey Titan!! Ni te he escrito para que no pierdas ni un minuto por Seattle ;) Si ves a mi hermano saludale, que anda por ahi también en el TechReady y de turisteo :) Por cierto, si el post es excelente es también en gran parte porque tu estuviste ahi currando como un campeon! Un abrazo, cra!

@Fran: Coño tio, ¡hacia cuanto que no escribias por aqui! Gracias a ti por leernos... a ver si te animas a postear un poquillo en un blog!

Gracias a los dos!!!

# February 5, 2009 10:40 AM

Tania ha opinado:

/clap

Me ha encantado el post :D Me hace darme cuenta de todos esos detallitos en los que no pienso mientras programo :-/

# February 5, 2009 12:19 PM

Sergio Tarrillo ha opinado:

Pablo, las fugas de memoria son más comunes en aplicaciones Windows?

Saludos,

# February 5, 2009 1:28 PM

Pablo Alvarez ha opinado:

@Tania: Gracias Tania!!! Todos tenemos lapsus a la hora de programar.. pero es que los lapsus vengan a la hora de diseñar :) Si es a la hora de programar, siempre tenemos tiempo para arreglarlo, y siempre podemos poner procesos para controlar, periodicamente, que no estemos metiendo errores grandes como perdidas de memoria.

@Sergio: No sabria decirte... creo que me he encontrado tantas fugas en WinForms como en ASP.NET/WS/WCF... en el fondo es la misma plataforma y los mismos desarrolladores, por lo que los fallos comunes aparecen igual en cualquier escenario.

# February 5, 2009 2:40 PM

JuanK ha opinado:

Hola, bueno muy interesante como siempre suelen ser tus post...

pero a todas estas: Quienes son "nuestros chicos de UX"?

saludos

# February 8, 2009 4:29 PM

Pablo Alvarez ha opinado:

@JuanK: Gracias por tus comentarios :)

Respecto a los chicos de UX, me refería a mis compañeros del equipo de UX (User eXperience) de Plain Concepts. Ellos son los que saben de diseño de interfaces y demás, por eso yo no les quiero pisar el terreno diciendoles que las aplicaciones MDI son la muerte peluda xD

# February 8, 2009 4:48 PM

JuanK ha opinado:

Sin lugar a dudas con esto ya te hiciste merecedor a MVPO por los próximos 10 años :P

excelente artículo.

# February 8, 2009 5:21 PM

Pablo Alvarez ha opinado:

@JuanK: XDD Gracias JuanK, pero sinceramente, tengo la suerte de convivir y compartir aventurillas con bastantes MVPs y hace falta mucho más que mis articulos para poder estar ahi ;) Aun asi, tengo la suerte de que puedo dar charlitas y eventos, escribir y responder, así que ya tengo todos los beneficios... no neceisto nada mas :)

Por cierto, me he pasado por tu blog... muy recomendable! Buen trabajo!

# February 8, 2009 5:29 PM

Irak ha opinado:

Bueno, me presento, como un nuevo interesado en tu página web, me sorprendo mucho con los temas tan profundos y avanzados que son manejados aqui como asuntos complejos con soluciones simples. Soy desarrollador en .NET y tambien en SQL Server 2000 y 2005, Espero aprender mucho de ustedes.

Felicitaciones por esto que es una obra

# April 8, 2009 12:10 AM

cesver ha opinado:

Hola Pablo, disculpa por revivir este post a pesar de tanto tiempo, pero desarrolle una aplicación MDI y me sucede que cuando el usuario cierra la aplicación el proceso sigue vivo; ¿es posible que sea por lo que comentas aqui?; ahora, intente implementar la solución sugerida en el evento OnMdiChildActivate del formulario MDI, pero cierro la aplicación y aun sigue vivo el proceso; te agradeceria si me pudieras colaborar con alguna luz o una posible solución.

Feliz día .......

# August 18, 2010 7:15 PM