Llamar o no a GC.Collect directamente

Hace ya un tiempo publiqué un post donde hablaba de liberación de memoria en código manejado, y cómo ayudar a quién es nuevo en .net y le dijeron que la memoria se liberaba sola. En éste, hacía una aclaración, la cual sigo manteniendo hasta ahora. Esta dice relación con el llamar o no a el metodo collect del garbage collector (GC.Collect()).

En esa oportunidad decía que uno jamás debería llamar al recolector de memoria manualmente, sino dejar que la recolección se hiciera en su tiempo, permitiendo que el garbage collector se adiestre sólo. También abría la posibilidad de que alguien diera una buena justificación para hacerlo. Hasta el día de hoy no había dado con ninguna, hasta ahora. Hago hincapié en que no me creo ni soy dueño de la verdad, pero me baso en lo que veo y leo día a día.

Podcast

Estando suscrito a podcasts, rss y cuanta información podamos y creamos que vamos a ser capaces de digerir en algún momento, algunos fueron recibiendo polvo hasta que un largo viaje arriba de un avión me permitió escucharlos, en mi Zune :). Si, tengo un Zune, igual que muy poca gente en este mundo porque nadie la conoce. Bueno, volvamos al grano.

El podcast pertenece a la serie de arcast.tv, el título de éste no era muy atractivo, no así la descripción.

El título decía “Steve Michelotti of e.imagination on High Performance Web Solutions”. Al igual que muchos, tu seguramente habrás encontrado documentación o podcasts con títulos impresionantes, que terminaban siendo una recopilación de recomendaciones que puedes encontrar en cualquier buscador, utilizando tres palabras como web, performance y .net.

La descripción por otra parte, detallaba con lujo de que se trataba. El tema principal era problemas con el recolector de basura en arquitecturas de 64 bits. No voy a incluir acá la descripción porque no podría justificar el contenido de lo que escribiré ahora y perdería la gracia.

Si el inglés no te abandona, te recomiendo que veas el podcast, como otros también en el mismo sitio. El de RESTful services está muy bueno, al menos para los que creemos en REST y sus cualidades. Si ves el podcast, puedes evitar seguir leyendo ésto.

¿Entonces?

Si no vas a ver el podcast y sigues acá, te cuento de qué se trata. No haré una traducción literal porque no tendría sentido, sino algo donde explique los puntos y complemente la información.

Mencionaba hace un rato que el garbage collector se adiestra automáticamente de acuerdo al patrón de petición de memoria de tu aplicación (allocation pattern en inglés). Para el caso de el sitio mencionado, varios problemas sacudían el día a día, todos relacionados con la petición y liberación de memoria. Veamos cada uno punto a punto y cómo los fueron tratando.

Gran cantidad de objetos creados en memoria

La aplicación era capaz de crear miles, millones de objetos en breve tiempo, los cuales eran descartados una vez que se dejaban de usar. Esto, en general, es esperable en cualquier aplicación. Los objetos se crean, se usan y luego se le dejan al GC para que reclame la memoria. El problema de ellos es que creaban muchos, MUCHOS objetos, aumentando la administración de memoria y disparando la recolección de basura con una frecuencia más breve. Si no haz visto el post de adiestramiento del GC, te lo recomiendo para clarificar este punto.

¿Cómo mitigaron el problema?, fácil, o ni tanto. Reutilizando objetos en vez de dejárselos al GC para que liberara la memoria utilizada. Debo reconocer que cuando lo escuchaba, me costaba asumir que era una opción que alguna vez podría considerar debido a la cantidad de trabajo que involucra. De esta forma, se crean menos objetos y se destruyen menos. Por lo tanto, menos presión al GC.

¿Cuál es el problema con el GC?

Mmm, en teoría, ninguno. En la práctica, debido a que el espacio de memoria virtual de un proceso de 64 bits es muy grande, en dónde grande significa aproximadamente 8TB de espacio de memoria virtual en modo usuario y 8TB en modo kernel (con diferencias leves entre IA64 y x64), los bloques de memoria manejados por el garbage collector pueden ser muchos más que en una arquitectura de 32 bits, donde los espacios de memoria eran 2GB para usuario y 2GB para kernel.

Como los bloques son muchos más , para el caso de la aplicación en cuestión, si el GC debía hacer una recoleción completa, es decir, incluir las 3 generaciones y el heap de objetos grandes (Large Object Heap o LOH en inglés), esta podía tomar entre 5 y 8 segundos en completarse. Para recolecciones de la generación 0 y 1, no es un problema porque ambas generaciones estan un único segmento, que por lo general no supera los 64 MBs.

Es importante rescatar que mientras se realiza una recolección de memoria, todos los hilos que están ejecutando código manejado son detenidos. Esto impactaba sevéramente en el SLA (Contrato de nivel servicio) de ellos. Si cada cierto tiempo se debe “detener” un servidor entre 5 y 8 segundos, será dificil poder cumplir con un SLA de 99,99%.

Otras acciones

Entonces, a la reutilización de objetos mencionada, se le agregó además la verificación que tanto operaciones de boxing como unboxing fuesen mínimas en el código. Boxing y unboxing se refieren a convertir un objeto de un tipo específico (de tipo valor) al tipo object y viceversa. Esto ocurre generalmente fuera de nuestra vista, cuando utilizamos funciones o tipos que reciben object en vez del tipo específico. En el siguiente post se muestra cómo detectar y minimizar este patrón de uso.

Otra acción para minimizar el problema del consumo de memoria, fue utilizare object pooling, funcionalidad generalmente utilizada para mantener conexiones a la base de datos sin ser destruidas, con el fin de minimizar la creación y destrucción de los objetos, proceso que ya sabemos es costoso.

¿Dónde entra GC.Collect y el llamado de forma explícita?

Habiendo minimizado la creación y destrucción de objetos, el problema continuaba ocurriendo en menor medida, pero cuando ocurría, nuevamente se sufría de varios segundos sin servicio.

Implementaron entonces un mecanismo de broadcasting para la aplicación, con el que se informaba del estado de un servidor, a todas las aplicaciones corriendo en los servidores que componían la capa de aplicación.

Cada cierto período de tiempo, y cuando ya “se creía” que podría ocurrir una recolección de memoria completa en una aplicación del servidor X, ésta le avisaba a todas las aplicaciones de los servidores restantes que iba a recolectar basura, y que por lo tanto no le fueran enviados requerimientos. Luego, cuando estaba “fuera de línea”, llamaba manualmente a GC.Collect y una vez finalizada la recolección, avisaba a las otras aplicaciones que ya estaba “en línea”.

Si bien la solución ayuda a mitigar el problema, se basa en la creencia (y esperanza) de que pronto será recolectada la memoria, esto, con el fin de actuar antes y evitar el impacto de quedar fuera por varios segundos. Sin embargo, ya sabemos que la recolección ocurre de forma no deterministica. Sólo el GC sabe cuándo será recolectada la memoria… hasta ahora.

… Hasta ahora …

La ayuda final que le da el toque de elegancia al modelo de ellos, (que ya lo tenía, para ser justos), vino de la persona que desarrolla el GC (Maoni Stephens). Ella ayudó con la implementación de una funcionalidad que permite, como aplicación, ser notificado con un margen de tiempo especificable por uno, cuándo podría ocurrir la recolección de basura, permitiendoles a ellos no tener que “adivinar” o “creer” cuando ocurrirá la recolección, si no que tener certeza absoluta.

Esta nueva funcionalidad, más la implementación del mecanismo de broadcasting, de puesta y sacada de línea de las aplicaciones, se puede minimizar totalmente el impacto de éstas.

La funcionalidad del GC la pueden ver en el siguiente articulo de MSDN.

Al menos para mí, este es el primer caso donde veo que el llamado de GC.Collect() de forma manual es la solución a algún problema específico.

Saludos,
Patrick

Alto consumo de memoria y cursores de datos

Sorpresas te llevas en la vida, siempre. A pesar de lo que parezca, hoy no ando sermoneador ni nada por el estilo. Es sólo que no se me ocurre como comenzar este post así que escribo lo primero que se me ocurre [:)]. Total, lo interesante viene ahora.


Viaje de emergencia, aplicación ASP con [OOM], servicio interrumpido.
Resumen: problemas… un poco de entretención para unos meses muy aburridos.


Síntomas


Como mencionaba, tenemos una aplicación ASP que de vez en cuando lanza excepciones por falta de memoria, mas conocidos como error 500 ASP 147. Obviamente con el reinicio del proceso, todo vuelve a la normalidad, pero luego de la calma, llega la tormenta.


Como ya es costumbre, se capturaron algunos dumps de memoria cuando se estaba produciendo el error y se analizaron. Los resultados fueron sorprendentes, los que pasan a ser mostrados ahora.


Lo primero muy interesante es que el dump apenas sobrepasaba los 100 megabytes. Un dump contiene, sin entrar en grandes detalles, los datos privados del proceso y las librerías cargadas entre otras cosas. Si son un poco más de 100 megabytes, ¿cómo es posible que haya falta de memoria?. El administrador de tareas confirmaba que el proceso estaba utilizando algo más de 100 megabytes en working set y un poco menos en memoria privada


¿Entonces?


[windbg] entra a ayudarnos. Revisando el estado de cada heap, nos encontramos con lo que muestra el bloque de más abajo. Hay muchas columnas y muchos datos, pero fijemos la atención en las columnas que hacen mención a la memoria reservada y comprometida.


Recordemos que en un sistema operativo Windows, la memoria puede estar en tres estados: libre, reservada y comprometida. Para que una aplicación la pueda utilizar necesita primero reservarla, y luego comprometerla. Después de usarla, la debe des-comprometer (fea palabra, lo sé) y luego liberar (des-reservar, también es fea).





  Heap     Flags   Reserv  Commit  Virt   Free  List   UCR  Virt  Lock  Fast
(k) (k) (k) (k) length blocks cont. heap
—————————————————————————–
00080000 00000002 15360 13424 14512 2362 514 137 0 9d L
External fragmentation 17 % (514 free blocks)
00180000 00008000 64 12 12 10 1 1 0 0
002b0000 00001002 22080 9560 17536 1629 231 124 25 5b L
External fragmentation 17 % (231 free blocks)
Virtual address fragmentation 45 % (124 uncommited ranges)
00550000 00000002 1024 20 20 2 1 1 0 0 L
00690000 00001002 256 32 32 2 1 1 0 0 L
01bb0000 00001002 256 12 12 4 1 1 0 0 L
01bf0000 00001002 39872 11200 29608 722 62 60 0 7 LFH
01c30000 00001002 256 12 12 4 1 1 0 0 L
01c70000 00001002 256 12 12 4 1 1 0 0 L
<recortado>
02630000 00001002 256 12 12 2 1 1 0 0 L
02670000 00001002 256 12 12 4 1 1 0 0 L
02730000 00001002 64 32 32 4 1 1 0 0 L
02a20000 00001002 3328 2084 2396 110 23 16 0 0 LFH
02a60000 00001002 19968 7164 8076 35 6 10 21 0 LFH
02f60000 00001003 1280 1152 1152 2 1 1 0 bad
03470000 00001003 1280 512 512 1 1 1 0 bad
034b0000 00001003 1280 524 524 2 1 1 0 bad
034f0000 00001003 256 96 96 0 0 1 0 bad
03730000 00001003 1280 356 356 1 1 1 0 bad
03970000 00001003 1280 264 264 0 0 1 0 bad
049b0000 00001003 256 204 204 3 1 1 0 bad
049f0000 00001003 130304 128 300 63 11 12 0 bad
04a30000 00001003 441600 112 188 101 11 12 0 bad <-éste
04a70000 00001003 167204 352 424 264 49 67 0 bad
04ab0000 00001003 465716 128 36532 103 20 27 0 bad
04af0000 00001003 469708 164 1696 92 13 32 0 bad
04b30000 00001003 46312 324 328 254 47 65 0 bad
04b70000 00001003 9700 372 372 348 62 64 0 bad
039c0000 00001002 64 16 16 2 1 1 0 0 L
04c30000 00001003 256 148 148 92 36 1 0 bad
<recortado>

En el listado anterior, vemos que hay un par de heaps que han reservado (memoria en estado reservado) más de 400 megabytes, pero que sólo están utilizando (memoria en estado comprometido) un poco más de 100 kilobytes. Entre varios, el heap 04a30000, indicado más arriba en negrilla y con la palabra “<- éste”, es uno de los más grandes.


Veamos el detalle de este heap y sus segmentos, listados a continuación.





Index   Address  Name      Debugging options enabled
111: 04a30000
Segment at 04a30000 to 04a70000 (00010000 bytes committed)
Segment at 0f940000 to 0fa40000 (00003000 bytes committed)
Segment at 0fa40000 to 0fc40000 (00001000 bytes committed)
Segment at 100d0000 to 104d0000 (00001000 bytes committed)
Segment at 104d0000 to 10cd0000 (00001000 bytes committed)
Segment at 10cd0000 to 11cd0000 (00001000 bytes committed)
Segment at 11cd0000 to 13cd0000 (00001000 bytes committed)
Segment at 13cd0000 to 17cd0000 (00001000 bytes committed)
Segment at 17ed0000 to 1fed0000 (00001000 bytes committed)
Segment at 4dbd0000 to 55bd0000 (00001000 bytes committed)
Segment at 5bb60000 to 5eb60000 (00001000 bytes committed)

Mmm… mmm…mmm…mmm (esto me recuerda una canción de hace unos años), la mayoría de ellos no tiene más de 4 kilobytes usados para bloques de varios megabytes reservados. Si las matemáticas no te ayudan ahora, 1000 en hexadecimal es equivalente a 4096 en decimal.


Análisis de la situación


Recordemos que el manejo de la memoria lo realiza generalmente el sistema operativo aunque algunas aplicaciones pueden utilizar sus propios manejadores de memoria. Desde código ASP (VBScript) o Visual Basic 6.0, como también desde código manejado NO es posible trabajar a este nivel con la memoria. Lo anterior es un problema en un manejador de memoria.


Si no es ASP, VB. 6.0, ¿qué puede ser? (considerando que no hay componentes desarrollados por el cliente en C o C++)


La respuesta la da [DebugDiag]. Quien creo el heap es “Microsoft Data Access Runtime”, es decir, MDAC. Revisando la versión instalada, comprobamos que es la última con Windows Server 2003 SP2. El camino se pone difícil.


Investigación y resolución


Involucrando a las personas adecuadas, aprendimos que este comportamiento es considerado “esperado” cuando se cumplen las siguientes condiciones:



  • Se utilizan recordset del lado del cliente (client-side cursor)

  • Se obtienen muchos datos, muchos datos de una tabla

Ok ¿client-side cursor?¿que significa “muchos datos”?


“Cliente” es quien consulta la base de datos, que para este caso es IIS/ASP. En ese caso, los datos se llevan al cliente para ser luego procesados.


Después de investigar en el código, se encontró que una consulta estaba retornando más de 2 millones de registros. Eso es mucho [:)]


Reproducción


Decidido a demostrarlo, procedí a hacer unas pruebas con el siguiente código en mi “servidor.”



Y le agregué a mi tabla algo así como 4 millones de registros.


Después de varias ejecuciones, tanto en paralelo como en serie, los contadores de memoria reservada, comprometida y utilización de procesador mostraron esto:



Se puede ver que la memoria comprometida (verde) llegó como mucho hasta 300 megabytes, pero la memoria reservada (roja) aumentó sin mostrar intención de disminuir, llegando casi hasta 900 megabytes.


¿Cuál es la explicación a que no reutilice la memoria reservada y siga reservando más? Al menos yo no tengo la respuesta.


¿Que sucede cuando llegue a 2 gigabytes? [OOM]


Conclusiones


1.- Nunca desplegar “muchos” registros en una página. Mejor aún, nunca pedir muchos registros a la base de datos.


2.- Utilicen server-side cursors. Hagan la prueba con el mismo código y comparen los resultados. [;)]


Saludos,
Patrick

Traza de asp.net y el consumo de memoria

El siguiente caso a presentar está relacionado con el alto consumo de memoria de una aplicación. Como el título lo dice, está relacionado con el uso de la traza de asp.net (trace en web.config.)


El escenario era similar a lo descrito ahora. La aplicación analizada empezaba a consumir memoria y aunque tenía momentos donde la liberaba, la impresión general era que en el largo plazo, siempre subía. Este es un típico comportamiento de un memory leak o pérdida de memoria; la tendencia al alza, aunque con momentos donde baja un poco.


Como ya es tradición, un dump del proceso y mi amigo [windbg] nos darían las pistas necesarias. También nos apoyamos en performance monitor, pero hay veces que ya sabes que es una pérdida de memoria y éste no te dirá mucho más de lo que ya sabes.


Con el dump en mis manos, hacemos una revisión del proceso afectado y el estado de la memoria virtual del éste.


Recolección de información general


Tamaño del dump: ~300 MB. No es muy grande, pero tampoco es descartable. Tradicionalmente se espera a que la memoria privada del proceso llegue al menos hasta 500 MB para declararlo como sospechoso.


Información del proceso:





System Uptime: 180 days 15:52:41.140
Process Uptime: 0 days 2:08:23.000
Kernel time: 0 days 0:00:13.000
User time: 0 days 0:08:48.000

El sistema operativo lleva corriendo 180 días sin reiniciarse. El proceso en cuestión lleva del orden de 2 horas y 8 minutos corriendo y que ha consumido 13 segundos en modo privilegiado (kernel) y 8 minutos y 48 segundos en modo usuario.

Con 8 minutos de procesamiento ya llegó a 300 MB. Interesante.







El estado de la memoria virtual concuerda con el tamaño del dump, reflejado gráficamente en la imagen de la derecha, la cual pueden pinchar para agrandar.

Efectivamente hay aproximadamente 300 MB de memoria virtual, de los cuales casi 190 son de memoria manejada.


Memoria manejada


Haciendo una revisión de los heaps del GC, obtenemos la información de la siguiente lista. El servidor tiene 4 procesadores por lo que se crean cuatro heaps del GC, cada uno con sus propias generaciones, heaps efímeros y heaps de objetos grandes.





Number of GC Heaps: 4
——————————
Heap 0 (0x000d01c8)
generation 0 starts at 0x121135c8
generation 1 starts at 0x12109a44
generation 2 starts at 0x102d0030
ephemeral segment allocation context: none
segment begin allocated size reserved
0x102d0000 0x102d0030 0x121135d4 0x01e435a4(31,733,156) 0x00d79000
Large object heap starts at 0x202d0030
segment begin allocated size reserved
0x202d0000 0x202d0030 0x205d2168 0x00302138(3,154,232) 0x00cdd000
Heap Size 0x2245770(35,936,112)
——————————
Heap 1 (0x000d08d8)
generation 0 starts at 0x16009a28
generation 1 starts at 0x15eaacac
generation 2 starts at 0x142d0030
ephemeral segment allocation context: none
segment begin allocated size reserved
0x142d0000 0x142d0030 0x16009a34 0x01d39a04(30,644,740) 0x014c7000
Large object heap starts at 0x212d0030
segment begin allocated size reserved
0x212d0000 0x212d0030 0x21449a38 0x00179a08(1,546,760) 0x00e66000
Heap Size 0x1f2b4a0(32,683,168)
——————————
Heap 2 (0x000d13d8)
generation 0 starts at 0x24461404
generation 1 starts at 0x243e6e2c
generation 2 starts at 0x182d0030
ephemeral segment allocation context: none
segment begin allocated size reserved
0x182d0000 0x182d0030 0x1998c7dc 0x016bc7ac(23,840,684) 0x028ef000
0x242d0000 0x242d0030 0x24461410 0x001913e0(1,643,488) 0x033c4000
Large object heap starts at 0x2a2d0030
segment begin allocated size reserved
0x2a2d0000 0x2a2d0030 0x2a2d0030 0x00000000(0) 0x047df000
Heap Size 0x198fb8c(26,803,084)
——————————

Heap 3 (0x000d1ca0)
generation 0 starts at 0x087954cc
generation 1 starts at 0x083dd820
generation 2 starts at 0x1c2d0030
ephemeral segment allocation context: none
segment begin allocated size reserved
0x1c2d0000 0x1c2d0030 0x1df48b8c 0x01c78b5c(29,854,556) 0x02310000
0x08330000 0x08330030 0x087954d8 0x004654a8(4,609,192) 0x01fff000
Large object heap starts at 0x232d0030
segment begin allocated size reserved
0x232d0000 0x232d0030 0x2344ed50 0x0017ed20(1,568,032) 0x00e61000
0x0edc0000 0x0edc0030 0x0f195f40 0x003d5f10(4,022,032) 0x00c2a000
Heap Size 0x277ac50(41,397,328)
——————————
Reserved segments:
——————————
GC Heap Size 0x827b3ec(136,819,692)

De esta información se puede ver que hay muchos objetos en generación 2, lo que significa que hay objetos que no se han liberado y han ido envejeciendo más de lo necesario.


¿Qué hay en la memoria?


Haciendo un vaciado estadístico de los objetos, utilizando [windbg] y sos, obtenemos un listado como el de a continuación, donde se ha eliminado parte del comienzo y sólo se ha dejado el final, que contiene la información relevante para el caso.





MT        Count TotalSize Class Name
…..<recortado por el bien de esta página>…
0x028665f0 22,807 273,684 System.Xml.Xsl.EndEvent
0x02784adc 14,800 296,000 System.Data.DataRowCollection
0x027844dc 14,800 296,000 System.Data.DataRowBuilder
0x027862d4 14,911 298,220 System.Data.ColumnQueue
0x02782a74 11,824 331,072 System.ComponentModel.CollectionChangeEventHandler
0x0230159c 14,866 356,784 System.Xml.NameTable/Entry
0x02925aec 7,847 408,044 System.Xml.Xsl.XsltCompileContext
0x02926bfc 11,589 417,204 System.Xml.XPath.XPathChildIterator
0x02863d04 8,692 417,216 System.Xml.XPath.ChildrenQuery
0x027846d4 14,800 473,600 System.Data.RecordManager
0x0278001c 14,800 473,600 System.Data.ConstraintCollection
0x027842e4 14,913 477,216 System.ComponentModel.PropertyDescriptorCollection
0x0280cddc 16,754 670,160 System.Xml.XPath.XPathWhitespace
0x0280c924 10,044 682,992 System.Xml.XPath.XPathElement
0x02866564 22,807 729,824 System.Xml.Xsl.BeginEvent
0x0252ebe4 14,800 769,600 System.Data.DataColumnCollection
0x79bda488 14,813 888,780 System.Threading.ReaderWriterLock
0x79bab93c 17,498 909,896 System.Collections.Hashtable
0x02923770 28,877 924,064 System.Xml.DocumentXPathNavigator
0x027882e4 40,203 964,872 System.Data.Common.StringStorage
0x0280cba4 21,157 1,015,536 System.Xml.XPath.XPathAttribute
0x0278489c 29,600 1,184,000 System.Data.DataRelationCollection/DataTableRelationCollection
0x01ba2c28 17,561 2,839,896 System.Collections.Hashtable/bucket[]
0x0252e4b0 14,800 3,433,600 System.Data.DataTable
0x79ba2ee4 145,618 3,494,832 System.Collections.ArrayList
0x0278670c 54,976 3,518,464 System.Data.DataColumnPropertyDescriptor
0x01ba2964 24,879 5,024,760 System.Int32[]
0x0278513c 136,364 5,454,560 System.Data.DataRow
0x0252f5d0 54,760 7,009,280 System.Data.DataColumn
0x01ba26b0 1,262 8,513,840 System.Byte[]
0x000cff48 2,829 14,897,824 Free
0x79b94638 238,884 23,296,388 System.String
0x01ba209c 235,235 40,966,392 System.Object[]
Total 1,509,853 objects, Total size: 136,798,976

Existe un poco más de 1,5 millones de objetos en memoria, que consumen cerca de 136 MB, similar al resultado entregado en la lista anterior.


De esos 1,5 millones de objetos, existe una gran cantidad de strings y arreglos de objeto, como también de filas de datos y columnas de datos. Este comportamiento se presenta cuando se han creado Datasets y no se han liberado.


Un DataSet contiene filas y columnas, y en cada celda encontramos objetos de los tipos básicos como strings, enteros, decimales, objetos, etc. Por lo tanto, estos objetos que se ven al final no viven solos sino que están referenciados desde otro objeto, como podría ser un dataset. No significa que todos estén referenciados, pero con 136 mil filas (DataRow) y 54 mil columnas (DataColumn), varios de esos miles de objetos si lo están.


¿Cuantos DataSets hay?





MT         Count TotalSize Class Name
0x0252ce84 1,597 127,760 System.Data.DataSet
Total 1,597 objects, Total size: 127,760

Bueno, tomemos uno al azar y veamos donde nos lleva. Elegimos el objeto en la posición de memoria 0x0834d148.





Name: System.Data.DataSet
MethodTable 0x0252ce84
EEClass 0x0275b43c
Size 80(0x50) bytes
GC Generation: 2
mdToken: 0x0200003b (c:\windows\assembly\gac\system.data\1.0.5000.0__b77a5c561934e089\system.data.dll)
FieldDesc*: 0x0252c4b4
MT Field Offset Type Attr Value Name
0x0252c23c 0x4000583 0x4 CLASS instance 0x00000000 site
0x0252c23c 0x4000584 0x8 CLASS instance 0x00000000 events
0x0252c23c 0x4000582 0 CLASS shared static EventDisposed
>> Domain:Value 0x000c4ac8:NotInit 0x00138380:0x1c331c58 <<
0x0252ce84 0x40003d3 0xc CLASS instance 0x15cee704 defaultViewManager
0x0252ce84 0x40003d4 0x10 CLASS instance 0x0834d198 tableCollection
0x0252ce84 0x40003d5 0x14 CLASS instance 0x0834d220 relationCollection
0x0252ce84 0x40003d6 0x18 CLASS instance 0x00000000 extendedProperties
0x0252ce84 0x40003d7 0x1c CLASS instance 0x1c331c30 dataSetName
0x0252ce84 0x40003d8 0x20 CLASS instance 0x102d0224 _datasetPrefix
0x0252ce84 0x40003d9 0x24 CLASS instance 0x102d0224 namespaceURI
0x0252ce84 0x40003da 0x40 System.Boolean instance 0 caseSensitive
0x0252ce84 0x40003db 0x28 CLASS instance 0x1c2e69c8 culture
0x0252ce84 0x40003dc 0x41 System.Boolean instance 1 enforceConstraints
0x0252ce84 0x40003dd 0x42 System.Boolean instance 0 fInReadXml
0x0252ce84 0x40003de 0x43 System.Boolean instance 0 fInLoadDiffgram
0x0252ce84 0x40003df 0x44 System.Boolean instance 0 fTopLevelTable
0x0252ce84 0x40003e0 0x45 System.Boolean instance 0 fInitInProgress
0x0252ce84 0x40003e1 0x46 System.Boolean instance 1 fEnableCascading
0x0252ce84 0x40003e2 0x47 System.Boolean instance 0 fIsSchemaLoading
0x0252ce84 0x40003e3 0x2c CLASS instance 0x00000000 rowDiffId
0x0252ce84 0x40003e4 0x48 System.Boolean instance 0 fBoundToDocument
0x0252ce84 0x40003e5 0x30 CLASS instance 0x00000000 onPropertyChangingDelegate
0x0252ce84 0x40003e6 0x34 CLASS instance 0x00000000 onMergeFailed
0x0252ce84 0x40003e7 0x38 CLASS instance 0x00000000 onDataRowCreated
0x0252ce84 0x40003e8 0x3c CLASS instance 0x00000000 onClearFunctionCalled
0x0252ce84 0x40003e9 0 CLASS shared static zeroTables
>> Domain:Value 0x000c4ac8:NotInit 0x00138380:0x1c331c20 <<

Mmm, nada más que un dataset. No se si podría haber encontrado otra cosa [:)]. Veamos el nombre de éste, en la posición 0x1c331c30 (línea blanca arriba).





String: NewDataSet

[:s]. El nombre genérico no da pistas para encontrar alguna parte del código que lo esté creando. Vemos entonces quién lo está apuntando.





Scan Thread 10 (0x1cac)
Scan Thread 16 (0xa00)
Scan Thread 17 (0x1908)
Scan Thread 5 (0xa0c)
Scan Thread 4 (0x1dcc)
Scan Thread 19 (0x1a6c)
Scan Thread 3 (0x1584)
Scan Thread 2 (0x444)
Scan Thread 20 (0x15f4)
ESP:36ff548:Root:0x1c6b59a8(System.Threading.Thread)->0x15cecad4(System.Runtime.Remoting.Messaging.IllogicalCallContext)->0x15cecae0(System.Collections.Hashtable)->0x15cecb14(System.Collections.Hashtable/bucket[])->0x15cec81c(System.Web.HttpContext)->0x15cec608(System.Web.Hosting.ISAPIWorkerRequestInProcForIIS6)->0x182d81c4(System.Web.HttpWorkerRequest/EndOfSendNotification)->0x14339e70(System.Web.HttpRuntime)->0x182d7fac(System.Web.Util.Profiler)->0x14358454(System.Collections.ArrayList)->0x1d89d7b0(System.Object[])->0x834d148(System.Data.DataSet)
Scan Thread 22 (0x1098)
Scan Thread 24 (0xcd8)
…..<recortado por el bien de esta página>…
Scan Thread 67 (0x1938)
Scan Thread 54 (0x618)
Scan HandleTable 0xc9ec0
Scan HandleTable 0xcd008
Scan HandleTable 0x147008

El thread 20, que está procesando un requerimiento en la extensión ISAPI de asp.net en IIS6 (System.Web.Hosting.ISAPIWorkerRequestInProcForIIS6) tiene una referencia fuerte al objeto. Vemos también un HashTable, el Contexto y Runtime de http, los cuales se pueden considerar normales en el ciclo de vida de un requerimiento web.


Sin embargo, hay un invitado especial en esta lista de referencias. Éste es System.Web.Util.Profiler, del cual no hay mucha información disponible, pero después de examinarlo, ya podemos inferir de quién se trata. Veamos su contenido.





Name: System.Web.Util.Profiler
MethodTable 0x021c49d0
EEClass 0x0215912c
Size 28(0x1c) bytes
GC Generation: 2
mdToken: 0x0200029d (c:\windows\assembly\gac\system.web\1.0.5000.0__b03f5f7f11d50a3a\system.web.dll)
FieldDesc*: 0x021c4704
MT Field Offset Type Attr Value Name
0x021c49d0 0x4001076 0x8 System.Int32 instance 1478 _requestNum
0x021c49d0 0x4001077 0xc System.Int32 instance 7500 _requestsToProfile
0x021c49d0 0x4001078 0x4 CLASS instance 0x14358454 _requests
0x021c49d0 0x4001079 0x14 System.Boolean instance 0 _pageOutput
0x021c49d0 0x400107a 0x15 System.Boolean instance 1 _isEnabled
0x021c49d0 0x400107b 0x16 System.Boolean instance 1 _oldEnabled
0x021c49d0 0x400107c 0x17 System.Boolean instance 0 _localOnly
0x021c49d0 0x400107d 0x10 System.Int32 instance 0 _outputMode

Recordando las propiedades de la traza de asp.net, podemos ver asombrosas similitudes con las propiedades. Veamos la definición de la traza en el archivo web.config. La información oficial dice:





<trace
enabled=”true|false”
localOnly=”true|false”
pageOutput=”true|false”
requestLimit=”integer”
mostRecent=”true|false”
writeToDiagnosticsTrace=”true|false”
traceMode=”SortByTime|SortByCategory”
/>

El dump dice que la traza estaba habilitada (_isEnabled), no estaba local (_localOnly), que tenía marcado para almacenar hasta 7.500 requerimientos (segunda línea blanca) y que habían 1478 almacenados (primera línea blanca). El resto de las propiedades las pueden chequear ustedes. Recordemos que habían 1597 DataSets en memoria.


El objeto _requests es un System.Collections.ArrayList y los 1478 objetos dentro suman casi la totalidad de memoria manejada, que si bien recordarán, sumaba cerca de 136 MB.





sizeof(0x14358454) = 91,258,512 (0x5707e90) bytes (System.Collections.ArrayList)

Entonces, la condición de memoria elevada, se da en parte porque hay ~90 MB de objetos referenciados por la traza de asp.net. También hay una cantidad de importante de memoria (~70 MB) consumida por dlls y ensamblados (ver la primera imagen.)


Conclusiones


Las conclusiones que se pueden obtener en este caso son:




  1. No todos las pérdidas de memoria son producto de errores en la codificación.



  2. Si vas a habilitar la traza en producción, recuerda deshabilitarla una vez que termines.



  3. También, si no vas a poder leer la información de 7500 trazas, configúralo para que almacene menos.


Saludos,
Patrick

Signos vitales de un servidor: Parte I (memoria)

¿Qué sucede si uno no se siente bien?; en condiciones normales, uno visita al doctor para saber qué sucede y éste hace una revisión general. Si el doctor encuentra algo interesante, te pide hacer unos exámenes para saber con mayor detalle qué está sucediendo.


Llevando este mismo ejemplo al área de la informática, si tienes un servidor y presientes (o tienes total certeza) que algo no funciona como debiera, un buen punto de partida es determinar si los signos vitales de éste están bien.


El objetivo de este post es dar una pauta general para determinar cuando un servidor Windows 2003, cualquier versión, puede tener algún problema que afecte sus signos vitales. Hoy veremos los signos vitales de la memoria. Los siguientes posts abordarán otras áreas.


Un poco de contexto


Si has escuchado muchas veces la palabra contadores, y no tienes claro a que se refieren cuando lo escuchas, esta sección debiera aclararte un poco. Si tienes claro a que se refiere, puedes saltarte  esta sección.


Los contadores son entidades u objetos que mantienen información del funcionamiento de algún programa o sistema operativo (SO de ahora en adelante). Cada programa que utilizas —todos debieran, pero no siempre lo hacen— genera información con estadística y en tiempo real del funcionamiento de éste, la cual se le informa al SO. A su vez, el SO también genera información que almacena para que nosotros podamos consultarla y saber cómo está funcionando.


Los contadores se categorizan por objetos (por ejemplo, memoria, sistema, sql server, iis) y se pueden o no aplicar a instancias (procesos, sitios web, bases de datos, etc.)


Por ejemplo, un contador de importante SO es la cantidad de memoria disponible que le queda. Este se categoriza en el objeto Memory, y el contador se llama Available MBytes, que nos dice la cantidad de megabytes de memoria disponible. Los nombres de los contadores dependen del idioma del programa o SO. Si estás ejecutando un SO en español, el objeto podría llamarse Memoria y el contador, cantidad de MB disponibles.


La siguiente es una imagen de cómo se agrega este contador utilizando performance monitor (perfmon).



Luego, después de habar agregado los contadores, podrás ver una pantalla como ésta, donde la primera es la representación gráfica y la otra la textual, para la misma información.


 


 


Suponiendo que ya se entiende la idea, vamos entonces a ver la primera parte de cómo chequear la salud de un sistema operativo.


Grupos de contadores


Aunque los contadores ya están agrupados de acuerdo a los objetos a los cuales pertenecen, los volveremos a agrupar en clasificaciones más grandes, más generales. Tendremos entonces un grupo para colocar los contadores de memoria, uno para los del procesador, otro para el disco duro, otro para la tarjeta de red y otro para el sistema operativo. En otra oportunidad veremos los asociados a productos específicos como SQL, IIS o .NET.


Contadores de memoria


Los contadores de memoria son variados. Podemos encontrar desde la memoria utilizada por una aplicación, la memoria libre del servidor y la memoria utilizada por los componentes del Kernel del SO. Todas estas son importantes y están de alguna forma relacionadas.


Veamos cada uno de los contadores mencionados y contra qué compararlos (cuando aplica).


Memoria del sistema (incluido kernel)


La memoria libre del sistema la entrega el contador Available MBytes, del objeto Memory. El mismo contador existe retornando el valor en otras unidades como KBytes y Bytes. De acuerdo a la cantidad de memoria que hoy en día tiene un servidor, medir en bytes o kilobytes puede no ser necesario.


Diversa documentación podrán encontrar referente a este contador. Algunos dirán que con 10 MB disponibles, es momento de preocuparse. Otros dirán que 25 MB es una señal de preocupación. En general, menos del 10% de la memoria total instalada ya es reflejo de que al sistema le falta memoria. Por ejemplo, si mi servidor tiene 2GB instalados y le quedan 150 MB disponibles, significa que estoy consumiendo el 92,5% de la memoria. Sin embargo, otros contadores te ayudarán a determinar si esto es cierto o no (si hay o no un problema). Puede ser que tengas mucha memoria ocupada, pero el sistema funcione bien.


Otro contador importante es Pages/sec del objeto Memory. Este refleja la tasa de lecturas y escrituras a disco para resolver problemas de páginas no encontradas (hard faults) en la memoria física, y por ende, tiene que ir al archivo de paginación a buscarla (y guardar otra para hacer espacio).


La información disponible públicamente es muy variada. Desde sitios que dicen que valores de más de 100 son preocupantes. Otros dicen que más de 5 de forma constante es preocupante. Lamentablemente ninguna de esas medidas es agnóstica y depende de otros factores (como el disco). Una medida agnóstica se obtiene al multiplicar este contador por Avg. Disk sec/Transfer del objeto Physical Disk, en la instancia (disco) donde está el archivo de paginación. El resultado de multiplicar ambos corresponde al porcentaje de tiempo de acceso a disco. Si es mayor a 10%, está paginando mucho, lo que puede ser producto de falta de memoria.


Los contadores de memoria del kernel se deben comparar contra valores en unas tablas específicas. Estos son Pool Paged Bytes y Pool Nonpaged Bytes del objeto Memory. Las tablas se encuentran a continuación, tanto para Windows 2000 como Windows 2003 (clic para agrandar).







Windows 2003

Windows 2000 SP4

Los valores obtenidos para cada unos de los contadores, tanto para paged como nonpaged, corresponden a la cantidad de bytes de cada pool que ha sido utilizado. Si es menor al 50% del pool, todo está bien. Mayor a 50% y menor a 80%, es hora de preocuparse. Si es mayor a 80%, hay problemas.


¿Cómo se mide?


Si obtuviste un valor de 56 MB en el contador de paged pool bytes y tu sistema es Windows 2003 con 4 GB y estás usando /3GB, el tamaño total del paged pool bytes es de 258 MB (revisa la tabla). La tasa de uso (56/258) es cercana al 21%, lo que nos dice que nuestro sistema está bien. Lo mismo para nonpaged pool bytes, pero considerando el otro valor en la tabla.


El último contador relacionado con la memoria en el kernel corresponde a Free System Page Table Entries (PTEs), del objeto Memory. Los PTEs son estructuras internas utilizadas por el componente del kernel llamado Memory Manager y que tiene como objetivo administrar la memoria.


Si este contador se reduce mucho (menos de 15 mil o 10 mil), habrá problemas. Usualmente este contador se ve afectado por la utilización de la opción /3GB sin utilizar la opción /userva. Esta anécdota ocurrió en un caso donde no se utilizó /userva.


Después de haber visto las opciones para el sistema y kernel, podemos ver las opciones de memoria disponibles para las aplicaciones.


Memoria de aplicaciones


Para medir la memoria de una aplicación específica, tengo que agregar los siguientes contadores, y asociarlos con el proceso que quiero medir (instancia en la parte derecha de la ventana):




  • Objeto Process, contador Private Bytes



  • Objeto Process, contador Virtual Bytes



  • Objeto Process, contador Working Set


El primer contador corresponde a la memoria privada el proceso. El segundo contador, a la memoria virtual del proceso. Para un mejor entendimiento de los tipos de memoria que miden, te recomiendo la lectura de estos posts:



El working set corresponde a la cantidad de memoria privada (sumada a la memoria usada por dlls y otras estructuras) que está cargada en memoria real (física). A grosso modo, la suma de los working set de todos los procesos no podrá superar la memora física de tu máquina.


Lamentablemente no hay valores contra qué compararlos, pero de acuerdo a lo que vimos en el segundo post de la lista de más arriba, la memoria virtual no podrá superar los 2 GB en un sistema de 32 bits.


Si virtual bytes es muy elevado en comparación con private bytes, podría haber un problema de fragmentación de memoria. ¿Qué significa muy elevado?; tampoco hay una respuesta, pero unas 10 veces debiera ser preocupante, como también es preocupante acercarse a los 2 GB. Claro que las 10 veces dependerá de la memoria que use nuestro proceso. Si un proceso utiliza 10 MB de memoria privada, es normal que utilice 100 MB de memoria virtual.


Hay muchas cosas relativas, pero lo importante es monitorearlos en el tiempo.


Si virtual bytes sube y sube sin descender (o descendiendo menos de lo que aumenta), habrá un problema de fragmentación de memoria, aunque dependerá de cómo aumente private bytes también.


Si private bytes sube y sube sin descender (o descendiendo menos de lo que aumenta), tendrás un problema de pérdida de memoria o memory leak. Private bytes no podrá ser superior al máximo de memoria virtual de un proceso (2GB).


Otros contadores


Sin duda, hay muchos más contadores, pero para determinar signos vitales, con éstos basta.


Desde Santiago de Chile,
Patrick

Contadores de rendimiento de aplicaciones de 32 bits en sistemas de 64 bits

Durante el análisis de Microsoft.VisualBasic.dll tuve problemas para poder ver los contadores de rendimiento de la aplicación desarrollada con Visual Studio 2003 y que se ejecutaba sobre el Framework 1.1, en un sistema XP 64 bits.


En ese momento inferí que podría deberse a que la aplicación estaba compilada para 32 bits y se estaba ejecutando en un sistema de 64 bits. Es conveniente recalcar que el Framework 1.1 compila ensamblados sólo para 32 bits.


Hoy, después de un tiempo, me tope con el KB 922775. Para mi sorpresa, tiene un punto dedicado al problema que se me presentó (You cannot monitor 32-bit managed programs in the 64-bit version of Perfmon). En éste, se presenta un workaround para poder monitorear contadores de rendimiento de aplicaciones de 32 bits corriendo en sistemas de 64 bits.


Además tiene información relevante para reconstruir los contadores si alguna vez se corrompen.


Saludos,
Patrick

Microsoft.VisualBasic.dll, ¿Eres tan malo como dicen?

Algunos años atrás, todo lo relacionado con Visual Basic (VB) 6.0 tendía a ser menospreciado o subvalorado. Los desarrolladores que utilizábamos VB 6.0 no éramos los primeros en levantar la mano para decir orgullosos que lo utilizábamos, como sí lo hacían los que usaban C o C++.


Una pequeña fracción de esa baja estima se mantuvo aún cuando apareció .net. Era cierto que teníamos un nuevo lenguaje (o un lenguaje muy remozado) que permitía lograr cosas impensadas en VB 6.0, tales como programar realmente orientado a objetos, crear threads, o deshacernos por fin de los problemas de los componentes marcados como Apartment, pero seguía existiendo “algo.”


A mi entender, algunos de los problemas que NO ayudaron a la transición real de un lenguaje limitado a un lenguaje completo, se pueden desglosar en la siguiente lista:




  • La existencia Option Explicit en vb.net. No existe programación seria sin declaración e inicialización de variables.



  • La existencia de Option Strict en vb.net.



  • Microsoft.VisualBasic.dll, librería para ayudar a la migración de proyectos, que implementa esas terribles funciones como Len, Left, Right, Trim, Mid, Replace y otras.


Respecto a este último punto, siempre que tenía la dicha de ver algún proyecto usándola, terminaba recomendado que no se use y que use las propiedades de los tipos de .net. (Ej: en vez de usar Len(variable) usar variable.length).


Ante la obvia pregunta de ¿por qué no debo usarla?, venía una respuesta que obtuve de variados lugares, pero nunca comprobé empíricamente, y que sostenía que tenía menor rendimiento que las nativas de los tipos.


A veces incluso utilizaba [reflector] para justificar mi teoría, como muestro en la siguiente imagen. ¿Para qué usar Trim(variable) si al final lo que se ejecuta es variable.trim? Mejor hacerlo directamente.



En esta oportunidad he decidido hacer pruebas sobre las funciones más utilizadas de VB 6.0, pero que fueron reescritas en este ensamblado (Microsoft.VisualBasic.dll) y determinar bajo pruebas empíricas si son “tan” malas como aparentan.


Quiero hacer hincapié en algo muy importante. Las funciones antes mencionadas, implementadas en VB 6.0, y que califiqué como “terribles”, justifican el calificativo asignado. Muchas de ellas, si es que no todas, reciben como parámetro de entrada un tipo de datos variant y retornan un tipo variant.


Esta conversión a variant penaliza el rendimiento de la aplicación, y peor aún, si se realiza en en ambos sentidos (entrada y salida). Siempre hablando en el contexto de VB 6.0, algunas funciones tenían variantes que terminaban en $, como Left$, y que tenían la gracia de retornar un tipo string, pero seguían recibiendo un variant. No entiendo que costaba hacer una que recibiese string y retornase string, pero ya está. Ya fue.


Por suerte, ahora en .net, las implementaciones en Microsoft.VisualBasic.dll incluyen parámetros correctamente tipificados, con la consecuente mejora en rendimiento.


Vamos a la prueba y a los sorprendentes resultados.


Código a ejecutar


Todas las funciones a medir fueron llamadas desde una única función general para cada lenguaje, llamadas ProcesarComoVB6 y ProcesarComoVBNET. No es mi intención medir y comparar el rendimiento de cada función específica sino que medir en general que sucede si se utilizan funciones de Microsoft.VisualBasic.dll o las nativas de los tipos en forma directa.


Cada función general recibe una cadena de 41.000 bytes y la procesa siguiendo la misma regla, detallada en el código de más abajo.


El siguiente es el código a ejecutar, utilizando Microsoft.VisualBasic.dll



El mismo código, utilizando las llamadas nativas de los tipo



La prueba comprende la ejecución una cantidad de 200 veces cada una de las funciones, midiendo los tiempos en terminar de procesar todas las instrucciones. Antes de iniciar las 200 iteraciones de cada una de las funciones, estas son llamadas un par de veces antes para JITtearlas (bonita palabra, no?)


Resultados de las pruebas


Ejecutando la prueba en un proyecto desarrollado en Visual Studio 2003 con el Framework 1.1, compilado en modo release, se obtienen los resultados de la siguiente tabla:



















Ejecución N° Microsoft.VisualBasic.dll Métodos nativos
1 28,51 segundos 16,20 segundos
2 27,76 segundos 16,15 segundos
3 30,45 segundos 17,40 segundos

 


Wow…(no estoy haciendo propaganda a Vista [;)]), impresionante. No nos engañemos todavía. Aún hay mucho que descubrir.


Analicemos antes de seguir las funciones que estamos midiendo y qué podría justificar la diferencia.


Tanto Left, Right como Mid debieran tener costos similares a la representación en VB.NET utilizando Substring. Técnicamente lo que hacen es obtener un grupo de caracteres de una cadena. No hay mucha ciencia.


Conversiones de caracteres a números y viceversa no son costosos, o no debieran serlo. Entonces, Convert.ToChar y Convert.ToInt32 debieran tener similar rendimiento a Asc y Chr respectivamente. Esto último no es necesariamente cierto para el resultado de la transformación. Los resultados de Asc y Chr pueden diferir de los de Convert.* de acuerdo a la cultura que se utilice.


¿Que nos queda?…Replace…el dolor de cabeza de un procesador….


Nuevas pruebas, sin Replace


El siguiente es el resultado de las pruebas removiendo el Replace del final, de ambas funciones. Los resultados son nuevamente sorprendentes.



















Ejecución N° Microsoft.VisualBasic.dll Métodos nativos
1 7,59 segundos 7,46 segundos
2 9,20 segundos 8,73 segundos
3 8,98 segundos 8,07 segundos

 


Interesante, no? ¿Qué se puede concluir ahora?


Conclusiones aventuradas


Descartando la función Replace, como supusimos, no hay mucha diferencia en el tiempo entre utilizar el ensamblado Microsoft.VisualBasic.dll y las funciones nativas. Replace hace la diferencia. Veamos por qué.


Si se utiliza [Reflector] para ver el código de String.Replace se obtiene esto.



Si se utiliza Reflector para ver el código de Replace desde Microsoft.VisualBasic.dll, se obtiene esto, y pongan atención a la zona enmarcada en rojo. Ouch!! Eso afecta cualquier procesador.



Más aún, Strings.Split y String.Join son funciones implementadas en Microsoft.VisualBasic.dll y no las nativas del tipo string.


Consumo de recursos


Veamos ahora como anda el consumo de recursos y uso del procesador para cada ejecución. Para eso utilizaremos performance monitor y revisaremos contadores de CPU, proceso, excepciones en .net y memoria en .net.


Los contadores a medir son:




  • Uso del procesador para el proceso, tanto para tiempo de proceso de usuario como de kernel



  • Excepciones lanzadas por segundo



  • Colecciones en cada generación



  • Colecciones inducidas



  • Total de bytes utilizados



  • Bytes pedidos por segundo



  • Tiempo utilizado por el GC



  • Bytes privados del proceso


Grande fue mi sorpresa cuando los contadores de rendimiento de la memoria en .net, todos marcaban cero durante la ejecución. ¿El motivo? No se si es el motivo “oficial,” pero al menos a mi me bastó como auto-explicación. El Framework 1.1 no corre de forma nativa en 64 bits ya que este sólo puede generar código ensamblado de 32 bits. Como mi sistema operativo es 64 bits, es posible que la instrumentación de 1.1 no funcione. Por lo tanto, a mover todo al Framework 2.0 y Visual Studio 2005.


Nota: Si te preguntas por que no hice todo el post utilizando 2.0, la respuesta es porque quería mostrar cual es el impacto de correr aplicaciones 32 bits sobre 64 bits emulando 32, tanto para el Framework 1.1 como 2.0. También haré la prueba con 2.0 compilado en 32 bits, que es lo que viene justo ahora.


Compilando para 32 bits (x86) una aplicación .net 2.0


En esta oportunidad no haremos mucho análisis. Me interesa dejar los tiempos como referencia y poder comparar en los diferentes ambientes.


Versión con Replace




















Ejecución N° Microsoft.VisualBasic.dll Métodos nativos
1 23,32 segundos 25,15 segundos
2 23,29 segundos 25,16 segundos
3 23,28 segundos 25,46 segundos


Versión sin Replace




















Ejecución N° Microsoft.VisualBasic.dll Métodos nativos
1 7,40 segundos 7,37 segundos
2 7,17 segundos 7,09 segundos
3 7,18 segundos 7,78 segundos


¿Conclusiones?…mmm, no gracias.. No tengo nada inteligente que aportar, como tampoco quiero hacer suposiciones. Full 64 bits por favor!!


Compilando para 64 bits (x64) una aplicación .net 2.0


Veamos los resultados de la misma prueba compilado para 64, o mejor dicho, compilado para cualquier CPU. Total, una vez que se JITtee (bonita palabra nuevamente, no?), lo hará para el procesador en que se está ejecutando.


Versión con Replace




















Ejecución N° Microsoft.VisualBasic.dll Métodos nativos
1 14,07 segundos 19,35 segundos
2 13,98 segundos 18,79 segundos
3 14,25 segundos 19,12 segundos


Versión sin Replace




















Ejecución N° Microsoft.VisualBasic.dll Métodos nativos
1 5,86 segundos 5,96 segundos
2 5,93 segundos 5,68 segundos
3 5,79 segundos 5,75 segundos


¿¿¿¿Qué????


Parece que el equipo de desarrollo de VB.NET se tomó en serio las críticas y decidieron mejorar el producto.


Veamos con reflector el código de Replace de Microsoft.VisualBasic.dll, para la versión 2.0 del Framework.



Interesante. Cambiaron esa idea de hacer splits y joins e implementaron una función con ese fin específico. ReplaceInternal utiliza un Stringbuilder para cumplir su labor. Bien.


Volvamos ahora que tenemos contadores al análisis del consumo de recursos.


Consumo de recursos, V 2.0


Analicemos la utilización de recursos para ambas versiones, con y sin Replace. En todos los contadores colocaré el valor por defecto, que en algunos casos es el promedio y otros el máximo.


Versión con Replace



















































Contador Microsoft.VisualBasic.dll Métodos nativos
# Exceptions Thrown/sec 0 0
# Gen 0 Collections 23.236 15.134
# Gen 1 Collections 2 61
# Gen 2 Collections 0 12
# Induced GC 0 0
# Total Commited Bytes 1,3 MB 3 MB
% Time en GC 7,1 % 2,8 %
Allocated bytes/sec 1,1 GB (así es) 512 MB
% User Time 89 % 85 %
% Privileged Time 3 % 12 %
Private Bytes 23 MB 24,8 MB

 


Mini conclusiones versión con Replace


La utilización de métodos nativos consume menos recursos en general. Hay una reducción importante en las recolecciones de primera generación (gen 0), pero las recolecciones en esa generación son muy económicas, al igual que las de la generación 1. Las de generación 2 son caras y hay que evitarlas, aunque 12 es un número pequeño.


Lo anterior se traduce en que la versión con métodos nativos consume menos procesador recolectando “basura,” y genera mucho menos “basura” (Allocated bytes/sec).


En la versión con métodos nativos, podría existir una relación entre la cantidad de bytes commited y la cantidad de recolecciones de segunda y tercera generación (gen 1 y gen 2) ya que hay memoria que se mantiene ocupada más tiempo y no es liberada en recolecciones de primera generación. Esto es solo una suposición.


Versión sin Replace



















































Contador Microsoft.VisualBasic.dll Métodos nativos
# Exceptions Thrown/sec 0 0
# Gen 0 Collections 14.305 12.654
# Gen 1 Collections 0 52
# Gen 2 Collections 0 10
# Induced GC 0 0
# Total Commited Bytes 1,3 MB 3 MB
% Time en GC 9,3 % 8,2 %
Allocated bytes/sec 2,0 GB 1,5 GB
% User Time 93 % 94 %
% Privileged Time 4 % 4 %
Private Bytes 23,5 MB 25,9 MB

 


Mini conclusiones versión sin Replace


El consumo de recursos es equivalente. Llama la atención el aumento en ambas versiones de la cantidad de bytes pedidos por segundo (allocated bytes/sec).


El resto de los contadores no presenta diferencias importantes entre ambas funciones para la versión sin Replace.


Conclusiones


Las siguientes conclusiones puedo obtener del análisis.


1.- No correr aplicaciones 1.1 en 64 bits


2.- No correr aplicaciones compiladas para x86 en 64 bits


Tristemente, mi intención inicial de encontrar algún motivo para que no se utilice Microsoft.VisualBasic.dll en los proyectos no ha podido ser cumplida. [:(]


En el único escenario donde encarecidamente recomendaría no usarla es en aplicaciones 1.1, pero en aplicaciones 2.0, la diferencia es irrelevante.


Saludos,
Patrick.
 

/3GB, memoria de kernel y un sábado de locos

Si no considero las 3 horas que dormí del viernes al sábado, podría decir que llevo casi 48 horas despierto, o que llevaba hasta hoy en la mañana ya que pude dormir 5 horas más de 6 a 11 am (hoy es domingo en la tarde). ¿Cómo tanto?. La primera trasnochada fue por una fiesta de la oficina. La segunda, por un visita fugaz a un cliente en apuros. Mi día sábado fue extenuante y estresante para mi familia, cumpleaños, visitas, compra de pasajes, arreglo de taxis, reservas hoteleras, información adicional, ajustar otros viajes, una locura.


Menos detalles aburridos y vamos al caso


El escenario era similar al descrito a continuación:




  1. Aplicaciones COM+ configuradas por defecto, salvo que algunas de ellas estaban configuradas con diferentes identidades (usuario que será utilizado para ejecutar la aplicación Com+). Las opciones disponibles para identidad son Interactive, Local Service, Network Service o uno específico.



  2. Después de un par de días funcionando, cualquier aplicación COM+ que no estuviese funcionando y era requerida, fallaría al levantarse



  3. Si la aplicación no había sido detenida durante todo el período de tiempo, seguiría funcionando sin problemas, aún cuando otras aplicaciones no pudiesen levantarse.



  4. Si esta aplicación dejaba de ser requerida por más de 3 minutos (valor por defecto) y era bajada, un nuevo requerimiento trataría de levantarla y fallaría.


Claramente no estábamos en presencia de algún problema de código ya que  en ese caso, habría fallado siempre. Y más aún, nadie había hecho ningún cambio en los componentes en bastante tiempo.


Investigación del problema


Como en cualquier actividad de resolución de problemas, lo PRIMERO que hay que mirar es el event viewer o visor de sucesos (en español).


Dos tipos de eventos llamaban la atención sutilmente. No habían muchos registros de ellos, pero estaban en el momento adecuado, en el lugar adecuado.


Uno de los eventos estaba en el mismo momento cuando empezaban los problemas. El mensaje era el siguiente.




  • Failed to create a desktop due to desktop heap exhaustion


Adicionalmente, otros tres eventos habían ocurrido uno de estos días, los que terminaron de aclarar la situación. Este evento el 2019:




  • Event ID 2019: “The server was unable to allocate from the system nonpaged pool because the pool was empty.”


Serie de preguntas y respuestas


Ingeniero, es decir, yo [:)]: ¿Configuraron /3GB en boot.ini?
Contraparte: Si
Ingeniero: ¿y configuraron userva?
Contraparte: No
Ingeniero: mmm…interesante

Ingeniero: ¿Cómo andan las PTEs?
Ingeniero: perfmon -> Memory -> Free System Page Table Entries (PTEs) –> cercano a 2.000 … ouch


Conclusión


El Kernel se quedó sin memoria.


¿Ahh?…bajémoslo a la tierra.


En una arquitectura de 32 bits, la memoria virtual es un recurso medianamente escaso. No vamos a hablar de la cantidad memoria física del servidor. Si bien, mientras más memoria mejor, el problema a describir ahora no depende totalmente de la cantidad de memoria del servidor, ya que o bien tengas 2 o 20 GB, se presentará igual.


Con 32 bits, la memoria virtual, que tampoco tiene que ver con la memoria paginada en disco, está restringida a 4GB por aplicación. El valor 4GB se obtiene al elevar 2 a 32, o 2^32.


De esos 4 GB virtuales, 2 GB son usados por un proceso y los otros 2 por el kernel. Cada aplicación tendrá sus 2 GB virtuales y compartirán los 2 GB de kernel. Debido a esto último, algunos de ustedes recordarán haber visto en Task manager que un proceso nunca pasa de 1,7 o 1,8 GB. Bueno, la explicación es esa. Una aplicación no puede usar más de 2 GB de memoria virtual.


Existe un switch para que sí lo haga, pero con su consecuencia. Este switch se configura en el archivo boot.ini, y corresponde a la opción /3GB.


El impacto de la aplicación de éste es que del espacio virtual de 4 GB, 1 GB pasa de memoria de kernel a memoria de proceso de usuario. Entonces ahora un proceso de usuario puede llegar a usar 3 GB y el kernel queda drásticamente reducido a 1 GB.


SQL Server, Exchange, COM+ y los Application pool de IIS son algunos de los procesos que tomarán ventaja de este cambio y podrán llegar a 2,7 u 2,8 GB de memoria usada, lo que representa un incremento del 50%, y que tiende a ayudar bastante en algunos casos, pero en otros, produce problemas si no se usa adecuadamente (como éste).


¿El problema?


El kernel necesita memoria para poder funcionar, y 1 GB virtual no es suficiente en algunos casos. En el kernel se cargan drivers, el manejador de memoria, de procesos, controladores gráficos, el HAL, manejo de plug & play y otras tareas.


Además, el kernel maneja dos pool de memoria, uno llamado paginable y el otros no paginable. En ingles, paged pool y nonpaged pool respectivamente. ¿Recuerdan el evento 2019? Vuelvan a leer la descripción.


Al aplicar el switch /3GB, el tamaño de estos pools se reduce a la mitad, pudiendo entonces en determinados casos agotarse y poner en riesgo el servidor. Si el kernel no es capaz de obtener memoria de estos pools, varios problemas pueden ocurrir, a saber:




  • Problemas de interfaz grafica, en donde ésta no responde adecuadamente



  • Problemas de funcionamiento de algunos procesos, para nuestro caso, las aplicaciones COM+ no levantan.



  • Falla en procesar requerimientos vía red


Al aplicar /3GB se reduce también la cantidad de PTEs disponibles, lo que generará problemas con los requerimientos de IO, entre otros.


Identidades de aplicaciones


Complementando lo anterior, si recuerdan, en un inicio hablamos de las identidades de las aplicaciones COM+.


Este tópico no lo comprendo a cabalidad, pero puedo decir (con temor a equivocarme) que cada aplicación que es iniciada con un usuario diferente, requerirá que el sistema operativo cargue ciertos componentes para el usuario (desktop). Estos componentes incluyen:




  • Menús del sistema



  • Ventanas



  • Otra información.


Entonces, si tengo aplicaciones COM+ configuradas con X usuarios, podría llegar a cargar X desktops. ¿De donde se obtiene la memoria para cargar estos desktops? De la memoria del kernel.


Como sé que la información que acabo de entregar del tópico de Desktops es bastante limitada, prefiero incluir un link donde esta MUCHO mejor descrita. Aunque está en inglés, no está difícil. El link es http://blogs.msdn.com/ntdebugging/archive/2007/01/04/desktop-heap-overview.aspx.


Solución


Si tu aplicación no hará uso de 3GB, no incluyas la opción. No será necesaria.


Si tienes alguno de los servicios que mencioné antes y necesitas usarlo, deberás entonces usar además la opción userva en el archivo boot.ini.


Userva se utiliza para “mover” un poco de memoria de proceso de usuario a kernel. Realmente no mueve nada, pero el efecto final es que en vez de tener 3GB de usuario y 1GB de kernel, puedes configurar 2,7 GB y 1,3GB respectivamente, lo que ayudará a mitigar el problema, y en la gran mayoría de los casos, desaparecerá totalmente.


El KB referenciado al final muestra como configurarlo. Si bien dice que se deberá probar entre 2900 y 3030, si es necesario para tu sistema, podrás llegar a un valor menor. Mayores valores no tienen sentido.


Si estas usando SQL Server, utiliza mejor AWE por sobre 3GB. No expondrás el kernel a estos problemas.


Adicionalmente, estos links te podrán proveer mas información:


http://technet.microsoft.com/en-us/library/7a44b064-8872-4edf-aac7-36b2a17f662a.aspx
http://support.microsoft.com/kb/316739


Este otro está curioso: http://xentelworker.blogspot.com/2005/10/i-open-100-explorer-windows-and-my.html


Saludos,
Patrick.

Adiestramiento del Garbage Collector (GC) y contadores de rendimiento

Hace un tiempo posteé acerca del uso de liberación de memoria en el framework, post que podrás encontrar aquí, en donde mencionaba que el GC se auto adiestraba para funcionar eficientemente, y que por eso no es recomendable forzarlo a recolectar la memoria, sino que dejarlo a él que lo haga.


Hoy hablaremos de cómo se auto adiestra el GC para realizar recolecciones de memoria en una frecuencia medianamente determinada, con el fin de impactar lo menos posible el rendimiento de la aplicación.


Debemos tener en cuenta que el impacto de una recolección es importante y varía de forma directa de la generación sobre la cual se hace la recolección y de la modalidad de GC que esté configurada. Otro día hablaremos de las generaciones y de cómo se distribuyen en la memoria. Por ahora, consideremos que cada vez que se realiza una recolección de memoria, todos los threads que están ejecutando código manejado en la aplicación deben ser detenidos mientras dure el proceso, no así para los threads que ejecutan código no manejado, los cuales siguen trabajando. El bloqueo de threads no ocurre para todas las versiones del GC, sino que solamente para la versión Server y la modalidad no concurrente de la versión Workstation. La que no se ve afectada es la modalidad concurrente del GC en versión Workstation.


Como el costo de una recolección es importante, lo ideal es que no se hagan. Bueno, eso no es posible sin que la aplicación deje de funcionar, por lo tanto, hay que encontrar el punto intermedio. El encontrar esa periodicidad de limpieza es lo que se conoce como adiestramiento, es decir, como el GC se va adecuando a los requerimientos de memoria de tu aplicación, lo que se conoce en inglés como allocation pattern.


¿Cómo se auto enseña el GC?


Antes de llegar a eso, veamos cuales son los únicos motivos que fuerzan una recolección de memoria en una aplicación, al menos para el GC incluido en la versión 1.1 y 2.0 del framework. No estoy al tanto de cambios en las versiones 3.0 y 3.5 del framework, aunque tengo entendido que la 3.0 no tiene cambios sustanciales con respecto a la 2.0. La 3.5 es desconocida aún, para Abril de 2007.


Entonces, las recolecciones de memoria son gatilladas por cualquiera de estos tres eventos:



  1. Llamado a GC.Collect

  2. Falta de memoria en el sistema operativo

  3. Falta de memoria en la aplicación

Revisando cada punto:



  1. Según vimos en el post mencionado al comienzo del artículo, jamás debe llamarse a GC.Collect.

  2. Cuando el sistema operativo se está quedando sin memoria, le ordena a todas las aplicaciones .net que hagan recolecciones de todas las generaciones para liberar la mayor cantidad de memoria.

  3. Cuando digo falta de memoria en la aplicación, no me refiero a [OOM] sino que a al hecho de no haber espacio disponible en el bloque de memoria asignado a la generación 0 (que llamaremos de ahora en adelante M0), aunque aún haya mucha memoria disponible.

Descartando los dos primeros puntos ya que no estamos llamando a GC.Collect ni nuestro servidor está escaso de memoria, lo único que puede gatillar un llamado a GC.Collect, es que se cumpla el punto número 3.


Además, debemos considerar que casi todas las peticiones de memoria que hacemos en nuestra aplicación, serán resueltas con memoria del H0. Las que no siguen esta regla son las peticiones de memoria de grandes bloques (mayores a 85k).


¿Entonces?


Si se produce una recolección cada vez que se llena M0, la forma de acortar o alargar la frecuencia de recolecciones está “linealmente” relacionada con el tamaño del éste (M0). Uso comillas en linealmente ya que casi nunca una aplicación sigue siempre un mismo patrón de petición (allocation pattern).


El GC recolectará la memoria cada vez que se llene M0. Si el GC encuentra que está recolectando muchas veces en poco tiempo, lo que sabemos que impactará el rendimiento de tu aplicación, agrandará el tamaño de M0 y dejará de recolectar tan seguido.


De esta forma, el GC se va adiestrando y ajustando a la aplicación.


¿Se imaginan que ocurriría si los objetos de gran tamaño se pidiesen desde G0?


Se dispararían las llamadas a GC.Collect ya que el M0 se llenaría muy rápido.


Ejemplo


Veamos un ejemplo de una pequeñísima aplicación de consola en C# y analicemos cómo se comporta el GC con performance monitor.


El código de la aplicación de consola es el siguiente:





[STAThread]
static void Main(string[] args)
{
       System.Threading.Thread.Sleep(3000);
       for (int i = 0; i<1300;i++)
             Console.WriteLine (“Ejecución (” + i.ToString() + “) : ” + Consultar());
}
 

Y la función Consultar hace lo siguiente:





public static string Consultar()
{
       SqlClient.SqlConnection oConn = new SqlClient.SqlConnection(“…”);
       SqlClient.SqlCommand oComm = new SqlClient.SqlCommand(“select …”, oConn);
       oConn.Open();
       oComm.ExecuteScalar();
       oComm.Dispose();
       oConn.Dispose();
       string s = new string(‘c’, 2500);
       string g = string.Empty;
       g = s;
       g = g + s;
       System.Threading.Thread.Sleep(50);
       return string.Empty;
}
 

Estamos consumiendo memoria no administrada y administrada al mismo tiempo, además de realizar una pequeña concatenación de strings con el fin de molestar un poco al GC.


En performance monitor se puede observar el siguiente comportamiento.


Las líneas azules corresponden a todo lo que se realiza sobre la generación 0, las rojas sobre la generación 1 y las negras, como esperarán, para la generación 2. A su vez, las líneas gruesas corresponden a la cantidad de recolecciones de memoria, las delgadas al tamaño del heap y las punteadas (- – – – -) a la cantidad de memoria que se mueve de una generación a otra (sólo hay dos colores ya que no hay memoria que mover desde la generación 2).


Los contadores utilizados y su color, todos pertenecientes al objeto .Net CLR Memory de performance monitor,  son los siguientes:



  • # Gen 0 Collections

  • # Gen 1 Collections

  • # Gen 2 Collections

  • Gen 0 heap size

  • Gen 1 heap size

  • Gen 2 heap size

  • Promoted Memory from Gen 0, en línea – – – – – – –

  • Promoted Memory from Gen 1, en línea – – – – – – –






Inicialmente la aplicación comienza con heaps de 0 bytes para cada una de las generaciones. Esto se puede ver por las tres líneas delgadas de colores que parten desde la izquierda. Todas parten desde cero. (Click en la imagen para agrandar)



Ante la primera petición de memoria, y debido a que el GC no puede predecir cómo se realizarán los requerimientos de memoria en el futuro, éste solicita un tamaño ni muy chico ni muy grande para comenzar. Así, 2,7 MB de memoria son asignados al M0. Nada de espacio es asignado a M1 y M2 ya que en ellos no se hacen peticiones de memoria. Posterior a esa petición de memoria, se hacen 2 recolecciones muy seguidas, las que pueden verse en la imagen inferior, dentro del rectángulo verde. Las recolecciones se reflejan en la subida de la línea azul gruesa.







En conjunto con el aumento en las recolecciones, la línea punteada azul nos dice que se traspasó memoria de G0 a G1. Esto se ve reflejado en el aumento del tamaño de M1, representado por la línea roja delgada, dentro del rectángulo verde.



 


Nota: Debido a que la tasa más pequeña de refresco de performance monitor  es un segundo, como se producen 2 recolecciones casi seguidas, no es posible ver las variaciones con más detalle.


Nota: Técnicamente hablando, G1 y M1 son casi equivalentes entre ellos. G1 corresponde a los objetos (y su memoria) en generación 1 y M1 al bloque de memoria donde se alojan los objetos. Descartando los problemas generados por los objetos pinned, podemos decir que son equivalentes. Lo mismo para G2 y M2.


Recapitulando, tenemos un aumento en el tamaño de M1 (línea roja) debido a recolecciones de memoria que generaron un traspaso de memoria desde G0, que  es reflejado por la línea azul punteada. El GC ajusta el tamaño de M0 para encontrar esa frecuencia ideal de recolecciones.


La aplicación sigue funcionando, pero debido a que M0 es tan grande, el GC no necesita hacer recolecciones de memoria mientras quede espacio disponible en él. Después de varias peticiones de memoria, éste se ha llenado y es momento de hacer una nueva recolección.


Si miramos la siguiente imagen (click para agrandar), en el cuadro verde veremos que se produce una nueva recolección.








Analicemos los cambios por parte:



  • Debido a que no hay más espacio disponible en M0, se realiza una recolección, que se refleja en la línea gruesa azul

  • La cantidad de memoria traspasada de la G0 a la G1 (línea azul punteada) es igual al aumento del tamaño del M1 (línea roja delgada).

  • El tamaño de M0 se ajusta haciéndose más pequeño debido a que el GC determina que ocurrió mucho tiempo desde la última recolección y estima que deben realizarse más seguido.







La siguiente recolección es muy interesante. Miremos la imagen de la izquierda, la que incluiremos con zoom (click para agrandar), hecho por el programa Paint. Ya sé que está feo, pero sirve mucho. Nuevamente, lo interesante está en el rectángulo verde.


 


Analicemos los cambios uno a uno:



  • Debido a que no hay más espacio disponible en M0, se realiza una recolección, que se refleja en la línea gruesa azul

  • Debido a la recolección, la memoria que aún se está usando en G0, se traspasa a G1 pero ya no hay más espacio para crear un nuevo M0  y poder crear nuevas variables. Cuando esto ocurre, se repite el mismo proceso que se venía haciendo anteriormente en M0 pero ahora en M1. Hay que realizar una recolección en M1 para liberar espacio.

  • La recolección en M1 se ve reflejada por la crecida de la línea roja gruesa. Esta había estado en 0 todo el tiempo.

  • La cantidad de memoria traspasada de la G1 a la G2 (línea roja punteada) es igual al aumento del tamaño del M2 (línea negra delgada). En este caso, calzan perfectamente ambas líneas, obligando al performance monitor a hacer una línea extraña, una mezcla de rojo medio punteado y negro.

  • El tamaño de M0 se ajusta haciéndose más pequeño debido a que el GC estima que las recolecciones deben realizarse más seguido.

  • El tamaño de M1 se hace 0 ya que no hay memoria utilizada por objetos en G1 (se traspasó a G2)

  • El tamaño de M2 crece por primera vez. Ya hay objetos almacenados en la generación 2.

De ahora en adelante, el GC ha encontrado el tamaño ideal del M0 y todas las recolecciones se realizarán con una similar frecuencia. Tomen nota del tamaño de M0. La línea azul delgada no varía. Esto ocurre porque nuestra aplicación tiene un patrón de petición muy repetitivo. Veamos la siguiente imagen.



Si se fijan, al extremo derecho de la imagen anterior, se ve que hay una nueva recolección de G1. Veámoslo con el maravilloso zoom de paint.


Lo ocurrido aquí es muy similar a lo de antes:








  • Debido a que no hay más espacio disponible en M0, se realiza una recolección, que se refleja en la línea gruesa azul, que no se ve en esta imagen muy pequeña, pero si se ve en la primera imagen del documento

  • Debido a la recolección, la memoria que aún se está usando en G0, se traspasa a G1 pero ya no hay más espacio para crear un nuevo M0  y poder crear nuevas variables.


  • Cuando esto ocurre, se repite el mismo proceso que se venía haciendo anteriormente en M0 pero ahora en M1. Hay que realizar una recolección en M1 para liberar espacio. Noten que la cantidad de memoria traspasada de G0 a G1 es muy similar durante el último tiempo. Debido a esto no se pueden ver puntillas de la línea azul punteada.

  • La recolección en M1 se ve reflejada por la crecida de la línea roja gruesa. Esta estaba en 1 desde la vez anterior.

  • La cantidad de memoria traspasada de la G1 a la G2 (línea roja punteada) que no se ve ya que está tapada por las otras [:(], es igual al aumento del tamaño del M2 (línea negra delgada). Se ve que aumenta sutilmente.

  • El tamaño de M1 se hace 0 ya que no hay memoria utilizada por objetos en G1 (se traspasó a G2)

  • El tamaño de M0 sigue idéntico (línea azul)

Si este proceso se repitiese lo suficiente, en algún momento la G2 deberá ser recolectada.

¿Qué sucede si el patrón de petición de memoria no es tan lineal como en este ejemplo?

Supongamos que no vamos a pedir bloques de 2.500 caracteres sino que de cualquier valor entre 1 y 10.000.


El nuevo código queda así. En donde decía





public static string Consultar()
{
       …
       string s = new string(‘c’, 2500);
       string g = string.Empty;
       …
}

 


Se reemplaza ahora por lo siguiente.





public static string Consultar()
{
       …
       Random r = new Random((int)(DateTime.Now.Ticks % int.MaxValue));
       string s = new string(‘c’, r.Next(1, 10000));
       string g = string.Empty;
       …
}

Lo anterior hará que no siempre se pida la misma cantidad de memoria, dificultándole un poco más el trabajo al GC.








Revisando detalladamente la imagen anterior (hagan click sobre ella para verla más grande), hay dos cosas que vale la pena mostrar  y que se diferencian del anterior caso.


Estas son:



  • La línea azul delgada ya no es plana. El GC va haciendo variaciones en el tamaño de M0 para poder encontrar el óptimo. Sin ser pesimistas, pero como sabemos que difícilmente una aplicación sigue un patrón de petición fijo, sabemos que jamás lo encontrará. Sin embargo, podemos decir que tiene éxito acercándose al óptimo ya que las recolecciones se hacen en una frecuencia similar y el tamaño de M0 varía dentro de una vecindad.

  • Noten que en la imagen, en la parte  inferior se pueden ver unas puntillas de la línea azul punteadas, que corresponden a la memoria que es traspasada de G0 a G1. Esto complementa lo dicho en el punto precedente y se diferencia del caso anterior donde la cantidad de memoria que pasaba de G0 a G1 era constante (no se veían las puntillas).

Casos más complejos


Supongan ahora que tienen un servidor web con una aplicación realizada en .net y su sitio recibe miles de visitas y requerimientos por hora. ¿Cómo funciona el GC en ese caso y cómo se vería eso en performance monitor?


Como decían mis profesores del colegio: Tarea para la casa.


Patrick.
Santiago de Chile

Liberación de memoria en código manejado (¿Dispose, Finalize, Object = Nothing, GC.Collect?)

Para quienes venimos del desarrollo utilizando Visual Basic 6.0, una de las primeras cosas que nos enseñan al empezar a utilizar código manejado (framework), es que ya no es necesario liberar la memoria porque “.net lo hace por ti”. Esta última parte entre comillas, además de ser incorrecta en su definición, es muy engañosa/confusa para quién es nuevo utilizando el framework.

¿Por qué está incorrecta en su definición?

De partida, el decir que .net lo hace es tan amplio que pierde el enfoque y no queda claro quiénes son los actores involucrados.


El actor principal es el garbage collector (GC). Éste está encargado de reservar la memoria desde el sistema operativo (grandes pedazos de memoria) y administrar los requerimientos de memoria de nuestra aplicación (pequeños pedazos de memoria). Esta administración comprende las tareas de asignar la memoria que es requerida por nuestra aplicación, por ejemplo las variables, para posteriormente reclamarla una vez que se ha dejado de utilizar. Para más información, ver el siguiente post sobre el manejo de memoria.

¿Por qué es engañosa/confusa para desarrolladores novatos en código manejado?

Si el problema se mira desde 10.000 metros de altura y si todos los componentes que se usan están correctamente programados, podemos decir que el GC si libera la memoria por ti.


Veamos las sutilezas que hacen que lo anterior pueda no cumplirse.


El GC es no-determinístico. ¿Qué significa esto? Significa que éste limpia la memoria cuando ÉL estima que es necesario y no cuanto TÚ quieres que lo haga; su ejecución no está determinada por ti, ni se ejecuta siguiendo un patrón detectable desde código. Sí lo hace siguiendo un algoritmo de optimización/adiestramiento interno, pero no es fácilmente detectable por uno como desarrollador. Además, al ser un algoritmo que se va adiestrando con el tiempo, su frecuencia de ejecución no es siempre repetible o predecible.


Entonces, si nos vamos a 10.000 metros de altura y para un tiempo T >> 0, podemos decir que la memoria será reclamada (liberada) por el GC, un poco más tarde de lo que se haría en VB 6.0, pero será reclamada.


Por otro lado, el GC no libera la memoria conocida como no manejada, es decir, la memora que él no administró. ¿Quién libera esta memoria? La respuesta tiene matices, primero el desarrollador, pero si éste no lo hace, alguien debe hacerlo.


Cuando se programan componentes que manejan recursos no manejados, el programador DEBE implementar el [patronDispose], que incluye la interfaz [IDisposable]. Esto debe hacerlo sí o sí.


Programadores expertos podrán argumentar que no es necesario implementar el [patronDispose] o que se puede implementar a medias (ver sección de los problemas más abajo referente al mito del finalizador). Esto es cierto, pero dependerá del control que ellos tengan sobre el uso de los tipos (clases) generados por ellos. Desde el momento en que ellos no controlen quién usará sus tipos, será entonces obligación hacerlo ya que es un estándar esperable.


Yo, como desarrollador, debiera ser el responsable de liberar la memoria no manejada llamando al método Dispose una vez que he dejado de utilizar el objeto de ese tipo. Esto, a diferencia del funcionamiento del GC, liberará inmediatamente la memoria no manejada utilizada por el tipo.

¿Qué sucede si el desarrollador no llama a Dispose?

Entonces dependerá de varios factores el que afecte o no a mi aplicación. Veamos algunos de ellos.


Si quién desarrolló el tipo que estoy utilizando implementó el [patronDispose] de forma correcta, los recursos manejados serán liberados cuando se ejecute el finalizador. Éste, de la misma forma que el GC, es no-determinístico.


Al no ser determinístico, la liberación de los recursos no manejados podrá hacerse tan tarde que podríamos encontrarnos con [OOM] o quedarnos sin conexiones a SQL Server (esto yo lo he visto en algunos casos donde he trabajado).


Tristemente, si el código no implementó Finalize porque el desarrollador novato no supo que había que hacerlo o porque el experto confió en que quién lo iba a usar llamaría a Dispose, definitivamente nos encontraremos con [OOM].

¿Dónde empiezan los problemas?

Uno de ellos se da porque rara vez un programador implementa el finalizador. Esto se debe a la existencia de un mito/costo asociado a éste. Es totalmente cierto que tener un tipo que implementa el finalizador tiene un sobrecosto en rendimiento comparado a un tipo que no lo implementa. Ahora, es muy cierto también que si se implementa el [patronDispose] de forma correcta, existe una línea de código en la función Dispose que quita parte de ese sobrecosto. Veamos el código de ésta:






public void Dispose()


{


    Dispose(true);

    GC.SuppressFinalize(this);

}

 

Sin entrar en demasiados detalles, el sobrecosto mencionado de tener un tipo que implementa Finalize se debe a que cada vez que se crea una instancia de éste tipo, este nuevo objeto se agrega a una cola especial de procesamiento. Bueno, esta función SuppressFinalize saca la instancia de la cola. Entonces, al final, el único sobrecosto está en agregar y sacar una instancia de la cola. No se cuantificar este costo, pero puedo asegurar que es mucho menor a los problemas producto de memoria no liberada. Se debe entender que una vez que se ha hecho Dispose de los recursos no manejados, ya no hay necesidad de llamar a Finalize porque no hay nada que finalizar.


Por eso es importante que el desarrollador llame al método Dispose, ya que además de garantizar la correcta liberación de recursos no manejados, también se produce esta optimización. Ese es otro de los problemas. El desarrollador no sabe que tiene que llamar a Dispose porque le dijeron que .net libera la memoria automáticamente.


El último de los problemas se da por que el desarrollador escucha o lee [recmalas] o poco precisas y decide usar el mismo código que estaba tan bien justificado (es en sentido irónico) en este otro sitio. Esto se refiere al uso de GC.Collect.

¿Y si llamo a GC.Collect?

Llamar a GC.Collect no tiene ningún efecto positivo; más aún, los efectos negativos producto de la ejecución de ésta podrían impactar severamente el rendimiento y consumo de recursos. Esto tiene que quedar claro ¡ya!


Jamás debe llamarse a GC.Collect. Existen excepciones contadas con menos dedos que los que tiene una mano, en las cuales el hacerlo podría (potencialmente) tener beneficios, pero esas no están dentro de las labores de desarrollo tradicional. Para el trabajo día a día, GC.Collect no está dentro de las funciones a utilizar.

Si no puedo recolectar la memoria, igualo los objetos a nothing/null

El igualar los objetos a nulo tiene, en la práctica, un impacto mínimo en la liberación de memoria. MSDN define esto como:






In Visual Basic .NET, setting an object to Nothing marks the object for garbage collection, but the object is not immediately destroyed.

 

Es correcto. Si se iguala un objeto a nulo, lo único que se está logrando es marcándolo para que sea liberado, la próxima vez que se ejecute el GC, algo que ya sabemos que ocurre de forma no determinística y en una frecuencia no conocida por los desarrolladores. Entonces, ¿qué gano haciéndolo?


Existen, al igual que el llamado a GC.Collect, contadas ocasiones donde asignar nulo a variables puede ayudar, pero todo dependerá de las probabilidades.


Supongamos que estamos en una función donde se han creado objetos que consumen mucha memoria (datasets, colecciones, hashtables, etc.), y que en alguna de las líneas que vienen más abajo, se hará una llamada a una función que demorará bastante (Web Service, SQL Server, etc.). Si llegase a ocurrir una recolección de memoria mientras se ejecuta esta función larga, todos estos objetos grandes envejecerán (pasarán de generación en el GC) y no serán liberados ya que aún se están “usando”.


Ahí es donde se puede hacer la diferencia. Si yo sé que no se usarán más adelante en la función, entonces puedo igualarlos a nulo y en la eventualidad que se produzca una recolección, estos serán efectivamente recolectados por el GC (no envejecerán) y la memoria será liberada.


¿Qué otras opciones existen donde valga la pena hacerlo?


Yo al menos no conozco ninguna otra, lo que por cierto no significa que no haya.

¿Y si igualo a nulo y llamo a GC.Collect?

Nuevamente, no se debe llamar a GC.Collect salvo contadísimas excepciones, las que no hemos discutido aquí. Si tú has vivido una situación donde hayas podido demostrar fehacientemente que el llamado a GC.Collect produjo un resultado positivo, te ruego compartirla.


Ahora, si tienes otro punto de vista o situación vivida que difiera de lo que acabamos de ver, también te ruego compartirla.


Patrick.

Dispose en SPWeb, SPSite y SPListItemCollection, desarrollando Web Parts para SharePoint

Después de una extenuante semana de viaje viendo un caso fuera de Chile, el cual me obligó a estar offline casi todos los días, me doy un tiempo para escribir y dar a conocer los usuales problemas con que uno se enfrenta cuando analiza web parts que corren sobre SharePoint*.


Hasta hoy, he visto web parts desarrolladas que normalmente tienen pérdidas de memoria (memory leaks), principalmente de memoria no manejada pero también de memoria manejada.


Como consecuencia de lo anterior, el continuo uso de éstas (Web Parts mal codificadas), llevará irremediablemente a encontrarnos con [OOM] en nuestra aplicación. Una forma de sobrevivir ante este problema de pérdidas de memoria es configurar el pool de aplicación de IIS 6.0 o 7.0 para que recicle cuando la memoria consumida (privada o virtual) llegue a una cantidad de megabytes determinada. Para la versión 5.0, existe una aplicación llamada IIS Recycle que hace algo similar, aunque asp.net en IIS 5 tienes capacidades de reciclamiento.


El principal problema con el desarrollo de Web Parts para SharePoint es que, al menos hasta la versión 2003, SharePoint Portal depende mucho de componentes COM que utilizan memoria no manejada, y el desarrollador eso no lo sabe, no se da cuenta o definitivamente olvida liberar los recursos no manejados (que incluye llamar a Dispose cuando corresponda).


Si te interesa y quieres confirmar esto, dale una mirada con reflector al ensamblado Microsoft.SharePoint.Library.dll.


Adicionalmente al problema de la no liberación de recursos no manejados, algunas bibliotecas de SharePoint están programadas de tal forma de que si necesita un recurso y este no se ha instanciado (o se ha destruido adecuadamente), ésta lo vuelve a generar. Esto complica más el panorama ya que si un desarrollador fue cuidadoso y lo liberó, el posterior uso de otra función puede revivir el objeto. Esto no es cierto para todas las funciones, pero algunas tienen comportamiento peculiar. Veámoslo en acción (toda la acción que se puede tener en un blog [;)]).


Supongamos que un desarrollador obtiene una SPListItemCollection en un objeto oList y para liberar los recursos no manejados, llama responsablemente a oList.List.ParentWeb.Dispose(), pero si después decide obtener la cantidad de objetos de esta colección llamando a Count, se podrá vivir la situación detallada anteriormente. El llamado a Count revivirá el objeto SPWeb.


El método Dispose  de SPWeb llama a Close, que llama a el método a sobre at, que es de tipo Microsoft SharePoint.Library.a. Antes de seguir, ten en cuenta que ahora se enreda bastante más.

 




public void Dispose()


{


    this.Close();


}

 

public void Close()


{


    if (this.at != null)


    {


        this.at.a();


        this.at = null;


    }


}

 

Y luego, el llamado a Count de SPListItemCollection:






public override int get_Count()


{


    this.a(true);


    return this.c;


}

 

El código del método a (que está ofuscado) es:






private void a(bool A_0)


{


    string text;


    SPWeb web;

    a a; //Esto se ve mal, pero es producto de la ofuscación

    string viewXml;


    if (this.f)


    {

        web = this.b.Lists.Web;
       
a = web.l();
        <Cortado para abreviar…>t;

}

 

La variable web referencia ahora al valor de this.b.Lists.Web y luego se llama al método l (ele) (ofuscado también). Recordemos entonces que web referencia a un objeto SPWeb (Web) que está referenciado por un objeto SPListCollection (Lists) que no debe ser confundido con el objeto del cual llamamos a Count del tipo SPListItemCollection. Este último (SPListCollection) a su vez está referenciado por un objeto de tipo SPList (b), el cual es parte de nuestro objeto inicial, SPListItemCollection (¿no te dije que se enredaba?).


El método l (ele) es internal y el código que nos muestra reflector es el siguiente:






internal a l(){


    this.i();


    return this.at;


}

 

Como ven, está retornando la variable at. Esta misma variable era la que se había asignado a null cuando se ejecutó SPListItemCollection.List.ParentWeb.Dispose(). Conviene mostrar el código de ParentWeb disponible en SPList para aclarar toda la relación.


ParentWeb retorna el objeto SPWeb asociado a la variable m_Lists (del tipo SPListCollection).






public SPWeb ParentWeb


{


    get


    {


        return this.m_Lists.Web;


    }


}

 

Solo para demostrar lo anterior, el código de i y h se despliegan ahora. Verán que el objeto at es nuevamente generado si este es null.






private void i()


{


    if (this.at == null)


    {


        this.h();


    }


}


 


private void h()


{


    int num = this.b.i();


    bool flag = -1 == num;


    bool flag2 = null != this.b.g();

    this.at = g.a(!flag2, this.al, this.b.d(), ref num);  

    <Cortado para abreviar…>


}

 

Si te has mareado con tanto objeto, es entendible. Lo importante aquí es encargarse de liberar toda la memoria referenciada por los objetos de SharePoint.


Si estás desarrollado Web Parts, no puedes no leer el siguiente documento. Considéralo como lectura obligatoria para desarrollar web parts que vivan sin problemas. La dirección del documento en MSDN  es http://msdn2.microsoft.com/en-us/library/ms778813.aspx y éste detalla de forma casi perfecta como liberar toda la memoria no manejada cuando programas Web Parts.


¿Por qué no digo que está perfecto? Porque le faltó agregar parte del caso que vimos hoy. En ninguna parte del documento indica la liberación del objeto SPWeb que esta referenciado por SPListCollection y a su vez por SPList y finalmente por SPListItemCollection. Es cierto que podría subentenderse, pero no está explícito.


Eso sí, tiene gran detalle para mostrar con ejemplos, código que está mal escrito y como debe escribirse correctamente. Si pudiésemos contar siempre con ejemplos así de MSDN, sería fantástico. Incluso, da a conocer sutilezas de la implementación que pueden hacer que tu aplicación colapse si no sigues las recomendaciones. Como ejemplo, vean esta aclaración que aunque está en inglés, no es difícil de entender.

SPSiteCollection [ ] Index OperatorThe SPSiteCollection [] index operator returns a new SPSite object for each access. A SPSite instance is created even if that object has already been accessed.

Si bien hoy no hablamos de SPSite, la limpieza de éste es tan importante como la de SPWeb.

 

*Realmente no sé si las web parts corren sobre otra aplicación que no sea SharePoint [;)], ya sea en la versión para Windows 2003 conocida como Windows SharePoint Services, como la versión full llamada SharePoint Portal server (http://www.microsoft.com/latam/office/sharepoint/prodinfo/relationship.mspx).


Saludos,
Patrick