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

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

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.
 

Entre las excepciones y la flojera de los desarrolladores

Una de las recomendaciones importantes en el desarrollo de código es la “no utilización de excepciones para evitar realizar validaciones.”


¿A qué me refiero?


A usar try/catch para no tener que escribir código que valide algo. Total, si se cae, en el catch retorno que es falso, si no se cayó, entonces retorno verdadero. Así no es necesario codificar rutinas especiales.


Vamos al caso


Como ya es costumbre en mi trabajo, y de las buenas costumbres, las pruebas de carga muestran la peor parte de las aplicaciones, y esta no fue la excepción, aunque el culpable era código de un componente de terceros.


Pruebas de carga


Treinta minutos de ejecución de un script grabado con [ACT], 50 usuarios concurrentes, 30 minutos. No vamos a dar todos los detalles, pero el uso de la CPU estaba “fluctuante”, y tenía unas subidas a 100% por varios segundos.


El siguiente es el grafico de performance monitor. Como hay más de 30 minutos, los segundos que la aplicación pasaba al 100% solo se ven como picos, pero en algunos casos llegaba a casi 40 segundos.



Te podrá haber llamado la atención que el uso de la CPU está muy extraño, como que no es fluido, considerando que una prueba de carga debiera mantenerla ocupada constantemente. No tengo la explicación ahora, pero al final veremos algo diferente.


Mientras revisaba contadores varios, me llamó la atención la cantidad de excepciones que se producían a veces, por lo tanto, agregué el contador de excepciones y voila!!. Problema encontrado.



Asombrosa similitud, ¿no?


A tomar dumps en las excepciones para ver que está ocurriendo. Mmm, mejor que no. Con 184 excepciones por segundo, reventamos el servidor. Mejor aún, utilizo un archivo de configuración para AdPlus, que viene en [windbg], el cual configuro para obtener información de threads, threadpool, stacks manejados y no manejados y excepciones cuando yo le solicite, que será cuando la CPU esté en 100%.


¿Resultado?


Esta es parte del stack no manejado, de un thread manejado. Como este, habían 10 más, es decir, 11 threads lanzando excepciones, a una asombrosa tasa de 184 por segundo.






 22 Id: 3924.46b8 Suspend: 1 Teb: 7ffa5000 Unfrozen
ChildEBP RetAddr Args to Child
06ece448 7c827ad6 7c8063cb ffffffff 7921ace1 ntdll!KiFastSystemCallRet
06ece44c 7c8063cb ffffffff 7921ace1 00000000 ntdll!NtQueryVirtualMemory+0xc
06ece4ac 7c8123b4 7921ace1 00000000 793e8730 ntdll!RtlIsValidHandler+0x82
06ece520 7c834d44 06ecc000 06ece530 00010007 ntdll!RtlDispatchException+0x78
06ece800 77e52db4 06ece810 050d5008 e0434f4d ntdll!RtlRaiseException+0x3d
06ece860 7921af79 e0434f4d 00000001 00000000 kernel32!RaiseException+0x53
06ece8b8 7921aefc 1194c7f0 00000000 06eceb14 mscorwks!RaiseTheException+0xa0
06ece8e0 7921aeb0 1194c7f0 00000000 06eceb24 mscorwks!RealCOMPlusThrow+0x48
06ece8f0 79218cc2 1194c7f0 00000000 0219d0b4 mscorwks!RealCOMPlusThrow+0xd
06eceb24 792b1893 00000018 00000001 00000000 mscorwks!CreateMethodExceptionObject+0x67b
06eceb58 79274773 00000018 79274778 06ecec08 mscorwks!RealCOMPlusThrow+0x35
06eceb68 79338b01 0216137a 000000e7 06eceb90 mscorwks!StringToNumber+0x7e
06ecec08 04266804 06ecec14 0f1604f0 000000e7 mscorwks!COMNumber::ParseDouble+0x32


Esta es parte del stack manejado, del mismo thread.





0x06ece908 0x7c834cf4 [FRAME: GCFrame]
0x06ecec38 0x7c834cf4 [FRAME: ECallMethodFrame] [DEFAULT] R8
System.Number.ParseDouble(String,ValueClass System.Globalization.NumberStyles,Class System.Globalization.NumberFormatInfo)
0x06ecec48 0x79a14e0f [DEFAULT] R8
System.Double.Parse(String,ValueClass System.Globalization.NumberStyles,Class System.IFormatProvider)
0x06ecec84 0x79a0e3cc [DEFAULT] R8
System.Convert.ToDouble(String)
0x06ecec88 0x06a30741 [DEFAULT] [hasThis] ValueClass System.Drawing.StringAlignment
C1.Util.Styles.StyleContext.1M(String)
0x06ececc4 0x06a306cd [DEFAULT] [hasThis] ValueClass System.Drawing.StringAlignment C1.Util.Styles.StyleContext.GetGeneralAlignment(Class C1.Util.Styles.Style,String)
0x06ececd8 0x06a30246 [DEFAULT] [hasThis] Void C1.Util.Styles.StyleContext.UpdateStringFormat(Class C1.Util.Styles.Style,String)
0x06ececf8 0x0580f464 [DEFAULT] [hasThis] ValueClass System.Drawing.SizeF C1.Util.Styles.StyleContext.GetContentSize(Class C1.Util.Styles.Style,Class System.Drawing.Graphics,ValueClass System.Drawing.SizeF,String,Class System.Drawing.Image)
0x06eced40 0x06a31182 [DEFAULT] [hasThis] I4 C1.Util.Styles.StyleContext.GetContentWidth(Class C1.Util.Styles.Style,Class System.Drawing.Graphics,I4,String,Class System.Drawing.Image)

La información de la excepción se muestra en la siguiente caja, y es equivalente a la presentada en el stack manejado.






Exception 0f69dff0 in MT 79bacb74: System.FormatException
_message: Input string was not in a correct format.
_stackTrace:
00000000
00000000
79bbb998
79a14eb0 [DEFAULT] R8
System.Double.Parse(String,ValueClass System.Globalization.NumberStyles,Class System.IFormatProvider)
06e8eca0
79bac7b0
79a0e3cb [DEFAULT] R8
System.Convert.ToDouble(String)
06e8ed20
79bbf6b0
06a30740 [DEFAULT] [hasThis] ValueClass System.Drawing.StringAlignment
C1.Util.Styles.StyleContext.1M(String)
06e8ed24
0573bf28


¿Qué nos dice [reflector] del código en el método 1M?







No voy a entrar a analizar que hace o no el código.


Lo único que puedo vez rápidamente es que el desarrollador que codificó estas líneas tenia ganas de irse rápido a la casa, como muchas veces me pasó a mí [:)].


¿Por qué poner un try/catch en vez de hacer la validación que correspondería hacer?


¿Es tan difícil usar un expresión regular para validar decimales o doubles, algo así como “[-+]?[0-9]*\.?[0-9]+“?


Muchas de los try/catch que son codificados podrían ser reemplazados por validaciones con ifs y elses, pero consume mucho más tiempo aprender cosas nuevas, y seguramente las pruebas en el computador del desarrollador no detectaron este problema [:)].


Como no fue posible corregir el código defectuoso, ya que estaba en un componente de terceros, procedimos a hacer unos ajustes y remover parte de la funcionalidad que invocaba ese método.


Nueva prueba de carga


El resultado obtenido habla por si solo. Consecuentemente, el grafico del uso de CPU ahora muestra un comportamiento más esperado para una prueba de carga. Y la gran cantidad de excepciones se fueron.



Conclusión


Tarde o temprano, los problemas aparecen. Es mejor invertir un par de horas en aprender algo que te podrá servir muchas veces a futuro, y dejar la chapucería para otro tipo de actividades.


Estas conclusiones están medias pobres, pero se debe a que no hay mucho más que decir. Los gráficos hablan por sí solos.


Adicionales


Si te interesa aprender de expresiones regulares, existe un muy buen libro y muchos sitios en internet. Personalmente uso el libro Mastering Regular Expressions.


Otros sitios web interesantes:



Saludos,
Patrick.

¿Por qué no debo compilar en modo debug?, Parte III

Como lo mencioné al terminar el segundo post sobre por qué no debo habilitar debug=true en web.config, la tercera entrega vendría relacionada con optimizaciones a nivel de código IL.


Si no leíste los posts anteriores, te recomiendo hacerlos, aunque este no es la continuación de ninguno de los dos. Las direcciones son:



Bueno, veamos ahora las optimizaciones que se realizan, aunque siendo más ajustado a la realidad, el código generado en modo debug está des-optimizado y con potenciales “bugs”.


Lo anterior significa que en modo debug, se agregan operaciones a nivel de código de máquina, como también instrucciones de código a nivel de IL con el objetivo de apoyar el debugging y la edición en caliente (edit and continue). Utilicé la palabra bugs entre comillas ya que no son bugs reales, pero en un escenario no adecuado, se comportan como tal.


Cuando se compila en modo release, el código generado es de menor tamaño y mas rápido, es decir, optimizado, como esperaríamos que siempre fuese.


Para mi ejemplo, tomaré un problema causado por un ensamblado (assembly) compilado en modo debug, que es exactamente lo mismo que hace debug=true en un sitio web, salvo que tienen ambientes de impacto diferentes.


Dejaremos para la cuarta y última entrega, el análisis de código de máquina.


Excepción lanzada


La aplicación web, en momentos de mucha carga, empezaba a lanzar excepciones del tipo System.IndexOutOfRangeException, con el texto acorde “Index was outside the bounds of the array,” estado del cual no lograba salir hasta un reinicio del proceso.


El siguiente era el stack de ejecución al momento de lanzarse la excepción. Las direcciones de memoria son de un sistema de 64 bits.






# Child-SP RetAddr : Args to Child : Call Site
00 00000000`04c9dc50 00000000`7816c664 : ffffffff`fffffffe 00000000`04c9dd90 00000642`7fc2f008 00000001`7fffa790 : kernel32!PulseEvent+0x60
01 00000000`04c9dd20 00000642`7f4477db : 00000642`7f330000 00000002`00000585 00000000`c089c2d8 00000642`7f4508eb : MSVCR80!CxxThrowException+0xc4
02 00000000`04c9dd90 00000642`7fa0e4b2 : 00000000`04b833e0 00000000`06717520 00000642`7f521a48 00000000`00000003 : mscorwks+0x1177db
03 00000000`04c9ddf0 00000642`782caee2 : 00000001`7fffa790 00000642`781043b8 00000642`802a24c0 00000000`00000000 : mscorwks!PreBindAssembly+0x428b2
04 00000000`04c9df60 00000642`80261e22 : 00000642`7882fe08 00000000`c08982c0 00000001`7fffa790 00000000`c00f0a40 :
mscorlib_ni!System.Collections.ArrayList.Add(System.Object)+0x32
05 00000000`04c9dfa0 00000642`80282dc9 : 00000000`c089be48 00000000`c08982c0 00000001`7fffa790 00000000`c00f0a40 :
ASSEMBLY.NAMESPACE.CLASE..ctor()+0x82


Por cierto, el nombre del ensamblado y la clase han sido modificados. No se llamaban ASSEMBLY.NAMESPACE.CLASE.


Analicemos el stack de ejecución y la excepción


El constructor de CLASE, en la instrucción de offset 0x82 desde el inicio del método, estaba llamando al método Add de un ArrayList.


A su vez, el método Add, en la instrucción de offset 0x32 desde el inicio del método, se estaba lanzando la excepción.


La excepción es de tipo IndexOutOfRangeException, es decir, se está tratando de referenciar un ítem dentro de un arreglo, el cual no existe.


Código del cliente


Revisando el código en el constructor (.ctor) de la clase CLASE, sorpresivamente no se encontró nada relacionado con ArrayList. La clase CLASE es la clase proxy que se agrega a un proyecto, cuando se agrega la referencia de un servicio web . Con mayor razón, nadie ha tendría por qué agregar código ahí. ¿Entonces?


Reflector al rescate


Volcando los ensamblados desde dentro del dump y revisándolos con reflector, apareció este código:






public CLASE() //Equivalente a .ctor
{
    
__ENCList.Add(new WeakReference(this));
    this.Url = MySettings.Default.ASSEMBLY_NAMESPACE_CLASE;
    if (this.IsLocalFileSystemWebService(this.Url))
    {
        this.UseDefaultCredentials = true;
        this.useDefaultCredentialsSetExplicitly = false;
    }
    else
    {
        this.useDefaultCredentialsSetExplicitly = true;
    }
}


¿Y esa línea?… Interesante.


Necesitamos más pistas


Desensamblando Add de ArrayList, se obtiene el código de la siguiente sección, siendo la línea blanca la “responsable” del problema.






public virtual int Add(object value)
{
    if (this._size == this._items.Length)
    {
        this.EnsureCapacity(this._size + 1);
    }
    
this._items[this._size] = value;
    this._version++;
    return this._size++;
}


Ok, tenemos la línea responsable del error, que está en el código Add de ArrayList. Al evaluar el escenario posible, el error podía estar o generarse en cualquiera de todos estos puntos:



  1. Error en el código de Add

  2. Error en alguna otra parte del código, como CLASE..CTOR

  3. Algo más

El punto número uno se descarta rápidamente, y el motivo es el siguiente. No es que el código Microsoft este libre de bugs, sino que un error en Add de Arraylist habría sido reportado hace 346.432.194.385.037 años.[:)].


Nota aparte: Leí en el libro de Debugging de [Robbins], que si el debugging corresponde a la eliminación de bugs, el escribir código corresponde a la creación de bugs. Esto lo podemos interpretar como no hay código libre de errores.


El método .ctor de CLASE presentaba esa línea de diferencia, la causante de todo el problema.


¿Quién es __ENCList?.. más pistas por favor


Está definido como un ArrayList estático, es miembro de CLASE, e instanciado en el siguiente método:






[DebuggerNonUserCode]
static CLASE() //Equivalente a .cctor (sí, cctor con 2c)
{
    __ENCList = new ArrayList();
}


Interesante..un método estático, privado y decorado con un atributo de sospechoso nombre (DebuggerNonUserCode), el cual reconozco que no conocía hasta ahora. Recomiendo su investigación.
http://msdn2.microsoft.com/en-us/library/system.diagnostics.debuggernonusercodeattribute.aspx
 


Además, y algo muy importante cuando utilizas miembros estáticos, es la protección del acceso sobre éste. Que sean estáticos significa que es compartido por todos los threads del proceso. Entonces, se debe cuidar que cuando se haga escritura sobre una variable estática, se haga garantizando que nadie más (otros threads) pueda accederla mientras se realiza la operación. Esto es similar al uso de transacciones en algún motor de datos.


En este caso, el miembro estático no está protegido, lo que en inglés se conoce como thread-safe. El no cumplir con esto, puede llevarnos a situaciones de race conditions. Más información en http://support.microsoft.com/kb/317723/en-us


Recopilando pistas


Tenemos las siguientes pistas:



  1. Error en código que no es del cliente

  2. Código con error es agregado automáticamente por el compilador

  3. Clase estática con miembro estático que no está protegido

  4. Atributo de nombre sospechoso (debug)

Las pistas anteriores no permiten concluir nada en específico, pero dan indicios y suposiciones de donde estaba el problema y qué se debía revisar.


Efectivamente el proyecto web asociado a esta aplicación estaba compilado en modo debug (debug=true), y tanto la variable __ENCList como la línea en el constructor fueron agregadas por el compilador.


Habiendo realizado el cambio para compilar en modo release, y habiendo recompilado el proyecto, el problema no volvió a presentarse, y por supuesto, ni la variable ni la línea tampoco aparecieron en los ensamblados cargados en memoria.


Código de máquina


La revisión del código de máquina quedará para la cuarta y última entrega referente a esto.


desde Santiago, Chile
Patrick.

Concatenación ultra rápida en Visual Basic 6.0 (ejercicio mental)

Después de una agradable tarde de relajo en el hotel Hilton de Sao Paulo, he decidido “jugar” un rato en el computador y echar a andar el cerebro.


Aunque el desarrollo utilizado Visual Basic 6.0 es cada día menos, decidí inventar un algoritmo (o función) para concatenar string que fuese más rápida que las que he visto en internet. Honestamente no he recorrido todos los sitios web disponibles, pero me basé en algunos códigos encontrados en algunos sitios, a saber:



El primero está muy bueno porque implementa una clase StringBuilder con funciones similares a las de su homónimo en .Net. El segundo, a pesar de ser más rápido que primero cuando el número de elementos a concatenar es alto, es muy engorroso y cualquiera que lo quiera usar, deberá tomarse un tiempo para poder implementarlo cómodamente.


Bueno, como mencionaba, mi intención para esta tarde era tratar de crear algo que corra más rápido, y así hacer funcionar las neuronas que a veces se nos atrofian.


Revisemos las implementaciones sugeridas en ambos posts, limitado a la parte importante de la concatenación, para no tener páginas y páginas de código.


Foros del web.






Class StringBuilder
    Private Sub Class_Initialize()
        growthRate = 50
        itemCount = 0
        ReDim arr(growthRate)
    End Sub
   
    Public Sub Append(ByVal strValue)
        If itemCount > UBound(arr) Then
            ReDim Preserve arr(UBound(arr) + growthRate)
        End If
        arr(itemCount) = strValue
        itemCount = itemCount + 1
    End Sub

    Public Function ToString()
        ToString = Join(arr, “”)
    End Function
End Class


La base del funcionamiento de este algoritmo es la utilización de un arreglo de strings donde en vez de concatenar, los strings se van agregando en el arreglo. Posteriormente la función ToString junta todos los strings del arreglo en uno solo. Simple, elegante y funcional. Notable.


MSDN






Sub Concat(Dest As String, Source As String)
    Dim L As Long
    L = Len(Source)
    If (ccOffset + L) >= Len(Dest) Then
        If L > ccIncrement Then
            Dest = Dest & Space$(L)
        Else
            Dest = Dest & Space$(ccIncrement)
        End If
    End If
    Mid$(Dest, ccOffset + 1, L) = Source
    ccOffset = ccOffset + L
End Sub
Sub MidConcat(ByVal LoopCount As Long)
    Dim BigStr As String, I As Long
    ccOffset = 0
    For I = 1 To LoopCount
        Concat BigStr, ConcatStr
    Next I
    BigStr = Left$(BigStr, ccOffset)
End Sub


La base del funcionamiento de este algoritmo es la utilización de un buffer donde se va copiando el contenido a concatenar. Este buffer se agranda a medida que se necesita agregar más información. Es bastante interesante, pero muy engorroso.


Desafío


El desafío que me planteé es realizar un algoritmo que fuese más rápido que ambos. Para hacerlo hice una lista con las ventajas y desventajas de cada uno. En este caso, las ventajas no me interesaban mucho ya que no tenia como mejorarlas, pero si mantenerlas en mi implementación, y me interesé en analizar las desventajas.


Entonces, la principal desventaja de cada uno es:




  • Foros del web: si la concatenación es con muchos strings, el arreglo crece bastante. Esto hará que la concatenación final no sea óptima. Además, agrandar el arreglo muchas veces también tiene su penalidad.



  • MSDN: cuando se llena el buffer, hace concatenación de strings para agrandarlo.


Entonces, ¿cómo mejorarlo?. Como me interesa la elegancia del primero, utilicé la misma idea, pero en vez de tener un arreglo de muchos strings pequeños, implementé un arreglo de buffers más grandes, en donde se va copiando el contenido con Mid$, sin necesidad de concatenar. La concatenación se hace al final, pero de sólo una cantidad de buffers mucho menor a que si fuesen strings chicos.


Con este algoritmo:



  1. Evito tener un arreglo de muchos elementos que luego hay que concatenar.

  2. Evito la concatenación de strings para agrandar el buffer, ya que cuando se acaba el buffer actual, agrego un nuevo elemento al arreglo de buffers.

Código de mi súper ultra rápida concatenación de strings.






Dim oArrBlocks() As String
Dim cBlocksUsados As Long
Dim iOffset As Long ‘mantiene la posición del último carácter copiado en el último buffer del arreglo
Const cNuevosBlocks As Long = 10
Const cNuevosBlocksLength As Long = 1000

Public Sub Class_Initialize()
    cBlocksUsados = 0 ‘Se define el primer bloque a llenar
    ReDim oArrBlocks(cNuevosBlocks) ‘se crea el arreglo de bloques
    oArrBlocks(0) = Space$(cNuevosBlocksLength) ‘Se llena el primero de espacios
    iOffset = 0
End Sub

Public Sub Append(ByVal sAppend As String)
    Dim iTemp As Long
    Dim iLargoCopiado As Long
    Dim iLargoTexto As Long
   
    iLargoTexto = Len(sAppend)
   
    If (iLargoTexto > 0) Then
        ‘revisar si cabe en el bloque que se está usando como buffer
        If (iLargoTexto + iOffset <= cNuevosBlocksLength) Then
            Mid$(oArrBlocks(cBlocksUsados), iOffset + 1, iLargoTexto) = sAppend
            iOffset = iOffset + iLargoTexto
        Else ‘ si no, hacer ajustes y particionar
            ‘Se copia lo que cabe en el bloque actual
            iLargoCopiado = cNuevosBlocksLength – iOffset
            If (iLargoCopiado > 0) Then
                Mid$(oArrBlocks(cBlocksUsados), iOffset + 1, iLargoCopiado) = Left$(sAppend, iLargoCopiado)
            End If
            ‘se crea un nuevo bloque y se copia el resto ahí.
            ‘Hay espacios disponibles en el arreglo de bloques?
            iTemp = UBound(oArrBlocks)
            If (iTemp = cBlocksUsados) Then
                iTemp = iTemp + cNuevosBlocks
                ReDim Preserve oArrBlocks(iTemp)
            End If
            cBlocksUsados = cBlocksUsados + 1
            iOffset = 0
            oArrBlocks(cBlocksUsados) = Space$(cNuevosBlocksLength)
            ‘Se copia el resto en el nuevo bloque
            iLargoCopiado = iLargoTexto – iLargoCopiado
            Mid$(oArrBlocks(cBlocksUsados), iOffset + 1, iLargoCopiado) = Right$(sAppend, iLargoCopiado)
            iOffset = iLargoCopiado
        End If
    End If
End Sub

Public Function ToString() As String
    oArrBlocks(cBlocksUsados) = Left$(oArrBlocks(cBlocksUsados), iOffset)
    ToString = Join$(oArrBlocks, “”)
End Function


¿Y los resultados?


Para concatenaciones pequeñas de caracteres, cualquiera de los tres es bastante eficiente. Sin embargo, cuando la cantidad de elementos a concatenar supera varias decenas de miles, se empieza a notar la diferencia. La unidad de medición para la prueba son los ticks, tomados antes y después de cada simulación. Para ser justos con la concatenación bruta (str = str & str2), en los otros algoritmos se incluyó el tiempo de creación y destrucción de objetos en cada una de las pruebas pertinentes. Cada prueba se realizó 5 veces, ingresando en la próxima tabla, los valores promedio.











































Algoritmo\Cantidad strings 1.000 5.000 10.000 25.000 50.000 100.000
Concatenación bruta 6 216 1.436 38.003 166.180 *
Foros del web 3 6,4 9,4 50 124,75 400
MSDN 0 3 15,4 37,4 105,75 356,2
Mi algoritmo 0 3 3 9,6 27,25 53

Para la prueba de 100.000 concatenaciones decidí excluir la concatenación bruta porque probablemente todavía estaría corriendo. [:)]


El siguiente gráfico muestra una curva con los resultados para los 3 algoritmos. Nuevamente no incluí la concatenación bruta ya que el grafico tendría una sola línea visible. Los otros algoritmos se dibujarían como líneas planas casi pegados a cero.



Conclusión


Sin lugar a dudas, el algoritmo logrado, que combina lo mejor y corrige lo peor de los dos anteriores, posee un mejor rendimiento, en especial, cuando la cantidad de elementos a concatenar es bastante. Para concatenaciones breves, cualquiera de los tres es igual de bueno.


Lamentablemente, hoy en día, este algoritmo no será muy utilizado ya que como comenté al inicio, ya casi no se hace desarrollo sobre Visual Basic 6.0 y para .net existe el objeto StringBuilder.


Como consuelo personal, rescato el ejercicio mental realizado hoy y los resultados obtenidos. Da gusto saber que aún “la neurona” responde.


desde Sao Paulo
Patrick.

Configurando threads en machine.config

La configuración de threads y conexiones de asp.net es un tópico oscuro. De algunos libros o KBs se puede obtener información, pero a mi entender, ninguna de ellas explica claramente cómo deben configurarse las opciones disponibles.


Las opciones que hago mención son las que se encuentran en el archivo machine.config, dentro de las siguientes secciones:




  • system.net/connectionManagement, atributo maxconnection



  • system.web/httpRuntime, atributo minFreeThreads



  • system.web/httpRuntime, atributo minLocalRequestFreeThreads



  • system.web/httpRuntime, atributo aspRequestQueueLimit



  • system.web/processModel, atributo maxWorkerThreads



  • system.web/processModel, atributo maxIoThreads



  • system.web/processModel, atributo minWorkerThreads



  • system.web/processModel, atributo minIoThreads



  • system.web/processModel, atributo requestQueueLimit


Estas opciones mencionadas son esencialmente para 1.1 ya que para 2.0 se pueden auto-configurar algunas (processModel). Mi experiencia en un caso específico no fue satisfactoria con la auto-configuración, así que optamos por la configuración manual.


La documentación existente hoy no es aclaratoria. Un libro que me gusta usar bastante es Improving .net Application Performance and Scalability, y a pesar de que tiene algunas secciones dedicadas a esta configuración, personalmente no me gusta la propuesta realizada porque no explica algunos detalles importantes ni tampoco te guía de forma correcta a encontrar tu propia configuración.


Detalles del caso


Los requerimientos eran bien simples. “Quiero que mi servidor procese la mayor cantidad de requerimientos que pueda, y tenga un tiempo de respuesta aceptable”. Seguramente este es el requerimientos tradicional de cualquier persona, y hay varios puntos que son totalmente subjetivos, pero hicimos el mejor esfuerzo por hacer pedazos el servidor con requerimientos.


Configuramos los valores sugeridos en el libro antes mencionado, y que son los mismos mencionados en este KB. Estos los despliego en la siguiente imagen, capturada desde el archivo scale.pdf (libro) disponible para bajar en éste link. Además vienen los valores por defecto.



Imagen obtenida de la página 280 del libro mencionado


Como el servidor web que estábamos probando tenía 4 CPUS, los valores aplicados fueron 48, 100, 100, 352 y 304 respectivamente.


Con la excitación de haber realizado la configuración correcta, procedimos a hacer las pruebas de carga, con 100 usuarios simultáneos, desde 2 servidores al mismo tiempo (200 usuarios en total).


Resultados


Los requerimientos se empezaron a encolar y después de algunos segundos empezaron a ser rechazados por el servidor, el cual consumía no más allá de un 3% de la CPU.


El escenario anterior está documentado en varias partes. Un uso de CPU muy bajo y encolamiento de requerimientos, entonces debes modificar tu servidor para que procese más requerimientos.


Ya se lo que hay que hacer. Ahora, ¿cómo lo hago?


Las páginas 445, 446 y otras del libro sugieren modificar modificar algunos valores y monitorear, si tendrás olas de cargas, etc. Es en este punto donde la documentación no es suficiente.


Con esto no quiero decir que mi explicación va a ser mejor que la del libro o que haya que copiar los valores que nosotros definimos para este servidor. Bajo ningún criterio se deberán copiar ciegamente los valores que nosotros aplicamos. Primero, se deberá lograr total entendimiento de los parámetros, la aplicación que se está sirviendo y las implicancias de los cambios que se hacen, para luego, de acuerdo a un estudio y pruebas de carga, se determinen cuales son los adecuados para tu ambiente. Me interesa mostrar una forma de llegar a los valores para lograr un proceso repetible.


En nuestra prueba de carga, el contador de rendimiento Requests Executing del objeto ASP.NET Apps v1.1.4322 mostraba 48 constantemente, mientras los encolados (Requests Queued de ASP.NET v1.1.4322) crecía impetuosamente. Luego empezó a crecer Requests Rejected del contador ASP.NET v1.1.4322.


Nota 1: Recordemos que los requerimientos empiezan a ser rechazados cuando la cantidad de requerimientos ejecutándose (Requests Current) sobrepasa el valor de system.web/processModel/requestQueueLimit. Los requerimientos ejecutándose (Requests Current) corresponden a la suma de los ejecutando, encolados y en espera de ser enviados al cliente. Esto significa que los requerimientos serán rechazados antes de que se llene la cola.


Nota 2: Existe otra cola más específica para cada aplicación o directorio web. El largo de ésta se define con system.web/httpRuntime/aspRequestQueueLimit, y su valor debiera ser menor comparado con system.web/processModel/requestQueueLimit ya que esta otra es una cola de uso general del Worker Process.


Bueno, teníamos 48 requerimientos ejecutándose y varias decenas encolándose. Se esperaba recibir 100 requerimientos por segundo. A ese ritmo, nuestro destino se veía muy lejos y con nubes negras encima [li]. ¿Dónde estaba el problema?


Análisis


Los valores sugeridos en el libro y el KB definen, utilizando un criterio basado en condiciones generales, que la cantidad de requerimientos a procesar por CPU será 12. Esto se refleja en varios atributos. Vamos por parte. Se hará referencia a las páginas del libro donde se definen.




  • maxconnection: valor sugerido, 12*#CPU. Este atributo limita la cantidad de conexiones a recibir (página 445). Es decir, si he definido 48, no puedo esperar a recibir más de 48, por ende, termino encolando y luego rechazando requerimientos.



  • maxIoThreads: valor sugerido, 100. Corresponde a la cantidad de threads disponibles para operaciones de I/O (disco, red, etc.), definido en la página 280. Este contador se multiplica automáticamente por la cantidad de CPUs del servidor. Entonces, potencialmente tenemos 400 threads disponibles para procesar requerimientos de I/O, pero ya sabemos que más de 48 no pasarán, y no todos son de I/O, así que serán menos.



  • maxWorkerThreads: valor sugerido, 100. Lo mismo de el atributo maxIoThreads, pero para threads que procesaran requerimientos y trabajarán dentro del proceso. Seguimos limitados por 48.



  • minFreeThreads: valor sugerido, 88*#CPUs. Este atributo representa la cantidad de threads que quiero que estén libres. ¿De dónde sale el 88?. Es la resta del máximo disponible menos la cantidad de requerimientos que quiero procesar por CPU, que es 12. Si la matemática no falla, 100-12 es 88. [:)]. Como dice la página 282, esta opción efectivamente limita la cantidad de requerimientos a procesar a 12, si maxWorkerThreads es 100.



  • minLocalRequestFreeThreads: valor sugerido, 76*#CPUs. Como el nombre lo dice, limita la cantidad de threads disponibles para requerimientos locales, dejando sólo 12 disponibles, ya que como las matemáticas se me dan hoy, 88-12=76 [:)].


Otros atributos interesantes.




  • minWorkerThreads: valor sugerido, maxWorkerThreads/2. Este atributo define la cantidad de threads que estarán disponibles en caso de una ola de requerimientos, lo que en el libro se llama Burst Load, explicado en la página 282. Al igual que su contraparte max, es implícitamente multiplicado por la cantidad de CPUs.



  • minIoThreads: no hay un valor sugerido, pero se puede suponer un valor similar a maxIoThreads/2. Se utiliza para soportar olas de requerimientos. Al igual que su contraparte max, es implícitamente multiplicado por la cantidad de CPUs.



  • requestQueueLimit: valor por defecto, 5000. Definir a criterio. Por lo general, el valor por defecto será suficiente.



  • aspRequestQueueLimit: valor por defecto, 100.


Durante las pruebas, rara vez lo contadores de threads del pool de aplicación (w3wp.exe) sobrepasaron los 80. Consideremos los 48 trabajando, mas los del garbage collector (1*#CPUs), el finalizador, el thread de compresión http, RPC local, etc., y otros más de control.


Nuestros requerimientos fueron rechazados cuando sobrepasamos los 100 de aspRequestQueueLimit. El uso del procesador no superaba el 3%.


Ajustes


Como podrán suponer, severos ajustes se debieron realizar para permitir procesar más requerimientos.


El cliente decidió soportar 1.000 requerimientos procesando al mismo tiempo. Un gran número. Eso no significa que el servidor recibirá 1.000 requerimientos por segundo, sino que podrá procesar del orden de 1.000 requerimientos al mismo tiempo. La duración de cada requerimiento impactará en la cantidad que se ejecuten concurrentemente. Como mencioné antes, el cliente esperaba recibir 100 requerimientos por segundo.


Siguiendo esta definición, los contadores se redefinieron con los siguiente valores. Explicaremos el por qué de algunos de ellos, y algunos puntos en contra que se deben considerar.


Si no has leído el resto del post, te solicito que lo hagas. Copiar estos valores sin entender qué hacen, podrá hacer que tu servidor colapse.




  • maxconnection = 1000. Si no se permite tamaña cantidad de conexiones, no tendremos los requerimientos.



  • maxWorkerThreads = 250. La formula dice que el contador se multiplica por #CPU, entonces, para poder procesar tantos requerimientos, se necesitan muchos threads..



  • maxIoThreads = 250. Seguimos la misma formula.



  • minFreeThreads = 100. No existe formula que aplicar acá. ¿Cuántos threads se quieren libres siempre? ¿10%, 5%? definir a criterio.



  • minLocalRequestFreeThreads = 50. Si no voy a procesar requerimientos locales, ¿para qué quiero reservar threads?. Este valor debió ser más pequeño.



  • appRequestQueueLimit = 1000. Si se van a procesar 1.000 requerimientos concurrentes, y se espera una tasa de 100 requerimientos por segundo, de nada sirve una cola de 100 registros. Se quedará corta en algunos minutos o segundos.



  • minWorkerThreads = 80. Si se hubiese aplicado la regla, se debió haber definido 125, para tener 500 threads disponibles. No se justifica. (ver consideraciones más abajo)



  • minIoThreads = 100. Igual que minWorkerThreads, si se hubiese aplicado la regla, se debió haber definido 125, para tener 500 threads de I/O disponibles. No se justifica. ¿por que no definimos el mismo valor que minWorkerThreads?. Nadie sabe. [;)].


Resultados obtenidos


Los resultados obtenidos fueron los esperados. Aunque no se pudo hacer que el servidor consumiera más del 15% del procesador, se logró procesar una tasa promedio de 84 requerimientos por segundo, con un máximo de 252 requerimientos en un segundo. Los requerimientos concurrentes promediaron 700 con un máximo de 970.


Se llegó a 970 ya que se aumentó la carga hasta 500 conexiones simultáneas por cada máquina de prueba (2 máquinas). Esos pobres servidores colapsaron ejecutando el script de [ACT].


El punto lamentable de la prueba fue darse cuenta de que el servidor de datos no fue capaz de responder a la demanda. El DBA tendrá que entretenerse un rato afinando consultas. Esto se tradujo en que algunos requerimientos demoraran más tiempo del esperado.


Consideraciones importantes


Aunque ya lo he dicho varias veces, te ruego no copiar los valores entregados aquí ya que son para un caso específico y condiciones especiales. Este es un servidor de servicios web, en donde cada uno de los servicios web se ejecutan en unas decenas de milisegundos (si el servidor de datos tiene poca carga). Si el servidor sirviese páginas ASPX con lógica más compleja que una simple consulta a la base de datos o si sirviese archivos de gran tamaño, la historia sería diferente y esta configuración seguramente haría sucumbir al servidor.


El tener en un momento 970 threads ejecutándose generará una alta tasa de context switch, lo que impactará el rendimiento de cualquier servidor. Nuevamente recalco que las condiciones especiales de este caso permitían definir tal cantidad de threads. Además, cada thread podría llegar a consumir 1 MB de memoria, aunque por lo general no debiera consumir más de 256K. Esto hará que se consuman cerca de 250 MB de memoria sólo por los threads. También es posible que se generen bloqueos entre los threads en dependencia de como esté desarrollada la aplicación. Es precisamente esto último lo que está detallado en el KB que mencioné mas arriba.


Todo esto debe tenerse en mente al configurar el archivo machine.config.


Saludos,
Patrick.

¿Por qué debo definir "debug=false" en web.config?, Parte II

En la primera parte de este artículo revisamos parte del impacto de definir debug=”true” en nuestro archivo de configuración web.config. Dentro de lo estudiado, abarcamos la revisión de la carpeta temporal de asp.net y cómo se generan los archivos de código de las páginas y controles, para finalizar con la generación de los ensamblados que resultan de la compilación.


En ésta oportunidad cubriremos cómo actúa asp.net para manejar las modificaciones de los archivos que ya están compilados. Excluiremos por motivos obvios las modificaciones a archivos de configuración, como de la carpeta bin o configuración de IIS.


Antes de entrar de lleno en la modificación, revisemos todas las opciones disponibles para la compilación en asp.net 1.1.

Opciones de compilación 

En la primera parte, utilizamos elemento de compilación del archivo web.config que es creado por defecto:

<compilation defaultLanguage=”c#” debug=”false”    />
 

Sin embargo, existe una cantidad importante de opciones disponibles para la compilación. Veamos uno más completo para VB.NET y C# respectivamente.

<compilation defaultLanguage=”VB” debug=”true”
numRecompilesBeforeAppRestart=”?cantidad” explicit=”true|false” strict=”true|false”
batch=”true|false” batchtimeout=”?seconds” maxBatchGeneratedFileSize=”?kilobytes

maxBatchSize =”?cantidad” tempDirectory=”?ruta”>
</
compilation>

<
compilation defaultLanguage=”c#” debug=”true”
numRecompilesBeforeAppRestart
=”?cantidad”
batch=”true|false” batchtimeout=”?seconds”
maxBatchGeneratedFileSize=”?kilobytes”
maxBatchSize=”?cantidad” tempDirectory=”?ruta”>
</compilation>

 


El significado de las opciones es a veces intuitivo y otras no. Veamos una por una, excluyendo las dos primeras que ya conocemos:




  • numRecompilesBeforeAppRestart: Se refiere a la cantidad de re-compilaciones dinámicas que se realizan antes de que se produzca un reinicio de la aplicación. Es trascendental aclarar que como re-compilaciones se refiere a cambios en los archivos de páginas, controles y otros recursos, dentro de los cuales definitivamente NO se incluyen los archivos de configuración (web.config, machine.config, etc.), los ensamblados de la carpeta bin y modificaciones al directorio virtual o aplicación web. El valor por defecto es 15. Aunque la documentación no lo dice, sí funciona para aspnet 1.1 (ver más).



  • explicit y strict: Las mismas opciones disponibles para los desarrolladores VB. Para C# no tienen efecto.



  • batch: Si es verdadero, se compilan los archivos agrupados de la mejor forma posible (como hemos visto hasta ahora). En caso de ser falso, se compila un archivo por página o control. No confundir con debug=false. En este caso se compila realizando las optimizaciones apropiadas al código.


  • batchtimeout: tiempo en segundos que se dará para que alcance a compilarse un grupo de archivos. En caso de demorar más tiempo del especificado, para ese requerimiento se cambia la modalidad a por archivo.


  • maxBatchGeneratedFileSize:  Según la ayuda, especifica el tamaño máximo (en KB) de los archivos de código fuente generados en cada compilación por lotes (batch). Me surge la siguiente duda. Si el archivo de código generado de una página de mi sitio es más grande, ¿no lo va a compilar?


  • maxBatchSize: Cantidad de archivos que incluirá como máximo en cada ensamblado.


  • tempDirectory: Directorio donde se copiarán los archivos para la compilación.
Reutilización de ensamblados

Algo que no habíamos comentado antes y que es muy importante. El resultado de la compilación (ensamblados) no se ve afectado por reinicios de proceso, de servicio o de servidor. Cuando se procesa un requerimiento se revisa si el recurso (aspx, ascx) está compilado en el directorio temporal, y en caso de ser así, se utiliza el ensamblado sin consumir tiempo y recursos compilando nuevamente.


La siguiente imagen fue capturada de mi equipo el 10 de Mayo después de hacer un cambio a un archivo y podrán observar que se han mantenido los otros ensamblados con fecha 07 de Mayo.



Si notan con cuidado, verán un archivo que tiene como extensión DELETE. Pasemos a revisar como ocurre eso.

Modificaciones de archivos

Para no distorsionar cualquier prueba, he limpiado mi carpeta temporal por lo que ahora cuento con archivos generados el día de hoy (22 de Mayo). La siguiente imagen muestra el estado inicial de la carpeta temporal.



Para complementar lo anterior, se despliega ahora una imagen con las DLLs cargadas en memoria para el proceso aspnet_wp.exe (worker process de IIS 5.1 de Windows XP 32 bits). Para monitorear procesos, pueden utilizar las herramientas disponibles de SysInternals.



Después de realizar una modificación en el archivo WebForm1.aspx, y volver a ejecutarlo, hacer otra modificación y volver a ejecutarlo, se pueden observar los siguientes cambios en la carpeta temporal y en el proceso aspnet_wp.exe.




En la primera modificación se generó un ensamblado para almacenar el resultado de la compilación de este archivo modificado (wdjnsrvz.dll). Como luego se volvió a modificar el mismo, el primer compilado (wdjnsrvz.dll) ya no era necesario y se generó uno nuevo (5jbqtzyx.dll). El ensamblado que ya no se utiliza, se marca con la extensión .delete.


Adicionalmente, en el proceso aspnet_wp.exe se continúan cargando ensamblados a medida que se van requiriendo páginas. Recordemos que los ensamblados NO pueden ser descargados de un proceso, a menos que se descargue el dominio. Para el caso de una aplicación web, el dominio es la misma aplicación, lo que hará que no se descarguen los ensamblados hasta que la aplicación se recicle.


Limpiamos nuevamente nuestra carpeta temporal, definimos debug=”true” y procedemos a modificar y forzar re-compilaciones.

Después de muchas modificaciones

Como era de esperarse, después de muchos cambios, la situación en el directorio temporal y el proceso no es de las mejores. Haz click en las imágenes para agrandar.








Una vez que modificamos los archivos más de las veces especificadas en numRecompilesBeforeAppRestart, el worker process (aspnet_wp.exe) se recicla, como era esperado.


Después del reciclado del proceso, este es el panorama en el directorio temporal y el proceso. Haz click en las imágenes para agrandar.







Con debug=”false”

Si repitiésemos todo los pasos anteriores, pero con debug=”false”, el resultado sería como el de la siguientes imágenes. Podrán ver que hay una menor cantidad de archivos, optimizados durante la compilación.














Si se revisan con detalle las imágenes anteriores, hay 4 ensamblados que no han sido modificados desde un poco más de 1 hora. Estos ensamblados son los que contienen los archivos Global.asax, WebUserControl2.ascx, WebUserControl1.ascx y WebForm5.aspx, que corresponden a los 4 archivos que no se han modificado en estas pruebas. Sólo se modificaron los WebFormN.aspx, con N desde 1 a 4.


A propósito, hemos comprobado que la opción numRecompilesBeforeAppRestart  sí funciona en asp.net 1.1.

¿Cuál es el valor adecuado para numRecompilesBeforeAppRestart?

Lamentablemente esta no es una pregunta sencilla de responder. En éste KB (http://support.microsoft.com/kb/319947/en-us) recomiendan definir un valor mayor la cantidad de archivos que actualmente se copian. Me imagino que hay algunos escenarios donde una recomendación de ese tipo hace sentido, pero en mi experiencia, la cantidad de archivos a subir a producción siempre es variable.

Otra implicancia de alto impacto

Existe una posibilidad de que una página pueda correr indefinidamente si  debug=”true”. De acuerdo al documento ASP.NET Performance Monitoring, and When to Alert Administrators, si está en true, el valor de <httpRuntime executionTimeout=/> y Server.ScriptTimeout serán ignorados, y dependerá del valor de <processModel responseDeadlockInterval=/> si se presenta la condición o no.

 

Espero en una tercera entrega, poder mostrar optimizaciones a nivel de código. Espero que ya estés medio convencido.


Desde Quito, Ecuador
Patrick

numRecompilesBeforeAppRestart sí funciona en Asp.Net 1.1

Mientras trabajaba en la segunda parte del artículo ¿Por qué debo definir “debug=false” en web.config?, Parte I, leyendo la documentación de MSDN en el link de más abajo, me encuentro con lo siguiente:


Link: http://msdn2.microsoft.com/en-us/library/system.web.configuration.compilationsection.numrecompilesbeforeapprestart(VS.80).aspx


El extracto del documento donde dice claramente que es una propiedad que es nueva para el Framework 2.0.





.NET Framework Class Library
CompilationSection.NumRecompilesBeforeAppRestart Property
Note: This property is new in the .NET Framework version 2.0.
Gets or sets the number of dynamic recompiles of resources that can occur before the application restarts.
Namespace: System.Web.Configuration
Assembly: System.Web (in system.web.dll)
 

Sin embargo, al utilizar esta propiedad en un sitio desarrollado en 1.1, es aceptada sin problemas en la configuración.


Más aún, haciendo una investigación más profunda, podemos ver con [Reflector] el código de System.Web y encontraremos este código en el método que procesa la sección de compilación:





    HandlerBase.GetAndRemoveBooleanAttribute(node, “debug”, ref result._debug);
    HandlerBase.GetAndRemoveBooleanAttribute(node, “strict”, ref result._strict);
    HandlerBase.GetAndRemoveBooleanAttribute(node, “explicit”, ref result._explicit);
    HandlerBase.GetAndRemoveBooleanAttribute(node, “batch”, ref result._batch);
    HandlerBase.GetAndRemovePositiveIntegerAttribute(node, “batchTimeout”, ref result._batchTimeout);
    HandlerBase.GetAndRemovePositiveIntegerAttribute(node, “maxBatchGeneratedFileSize”, ref result._maxBatchGeneratedFileSize);
    HandlerBase.GetAndRemovePositiveIntegerAttribute(node, “maxBatchSize”, ref result._maxBatchSize);
    HandlerBase.GetAndRemovePositiveIntegerAttribute(node, “numRecompilesBeforeAppRestart”, ref result._recompilationsBeforeAppRestarts);
    HandlerBase.GetAndRemoveStringAttribute(node, “defaultLanguage”, ref result._defaultLanguage);

 


Efectivamente la propiedad se lee del archive de configuración, y como era de esperarse, tiene su valor por defecto puesto en duro en la clase.





private CompilationConfiguration()
{
    this._batch = true;
    this._batchTimeout = 15;
    this._maxBatchGeneratedFileSize = 0xbb8;
    this._maxBatchSize = 0x3e8;
    this._recompilationsBeforeAppRestarts = 15;
}

 


Bueno, nada más que decir. Para tus proyectos en Framework 1.1, aún es tiempo de usarla. Para los de 2.0, 3.0 y 3.5, también puedes ya que la documentación dice que es de 2.0 en adelante.


Patrick.
Santiago