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

Uno de los atributos menos comprendidos del archivo web.config es el relacionado con la compilación (compilation). Cuando yo desarrollaba y estaba encargado de manejar sitios en producción, siempre me preocupaba de que el atributo estuviese en false en producción, ya que había leído que así debía ser y no me lo había cuestionado mayormente, fundamentado en lo que yo creía que significaba.


La principal confusión, al menos para mí, venía por lo que yo entendía por compilación. Cuando uno realiza un proyecto web en Visual Studio 2003, se puede generar código en dos modalidades. Una de ellas de Code Behind que corresponde a un archivo del lenguaje (VB.NET o C# por ejemplo) donde incluimos el código asociado a la página, y la otra modalidad es incluir directamente el código dentro de la página.


Siempre he sido un fanático del primer tipo de proyecto y su compilación, la cual fue drásticamente eliminada en Visual Studio 2005 y reemplazado por una cantidad casi infinita de posibles formas de compilar y generación de ensamblados (assemblies), no repetibles y que son bastante más engorrosas para subir a producción. Más adelante veremos esto y cómo Microsoft enmendó el camino.


Con la primera metodología para trabajar proyectos en VS 2003, al compilar, se genera un ensamblado en la carpeta bin del sitio web. Este ensamblado tiene todo el código de los archivos Code Behind compilado en su interior. Utilizando la otra forma, se genera también un ensamblado en la carpeta bin, pero éste contiene información mínima de la aplicación, como las referencias. Todo el código queda en las páginas y sube al sitio (con todos los riesgos que esto implica).


Bueno, volviendo a mi confusión, yo creí, al igual que varios que conozco, que esta opción debug=”true” del atributo compilation aplicaba sólo para los proyectos generados en la segunda modalidad y no para los con Code Behind, ya que todo estaba compilado en el ensamblado.


Ahí estaba mi error. Repasemos antes cual es el atributo al que estoy haciendo referencia. En el archivo web.config existe el siguiente bloque de configuración.

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

Además de la compilación que se realiza al presionar Build Solution o Rebuild Solution, y que genera este ensamblado en la carpeta bin, existe otra compilación que se realiza cuando la aplicación es requerida.








Tenemos una aplicación que tiene 1 página, igual a la que por defecto se nos propone al crear un nuevo proyecto web. Este proyecto se ve como el de la siguiente imagen.


 


Si genero el proyecto con build, se genera un ensamblado y su archivo de símbolos (.pdb) como vemos ahora:



Primer requerimiento


Antes de que mi aplicación sea requerida por un browser, nada ha ocurrido ni se ha compilado (además de lo que ya compilamos en la carpeta bin). Eso lo podemos ver examinando la siguiente carpeta:





%windir%\Microsoft.NET\Framework\v1.1.4322\Temporary ASP.NET Files







Seguramente encontrarás cosas que no pensabas que existían, pero en mi caso, no hay nada porque la limpié hace unos momentos. Entonces, si le hago un requerimiento a mi sitio http://localhost/WebCompilation/WebForm1.aspx, en esta carpeta aparece lo siguiente, después de expandir todos los directorios.



 


Las únicas carpetas que tienen información son:






…\Temporary ASP.NET Files\webcompilation\f7e6afd0\61bb37a7
…\Temporary ASP.NET Files\webcompilation\f7e6afd0\61bb37a7\assembly\dl2\a61b0a98\2a9e47df_4890c701
 

El contenido de la primera carpeta se despliega en la imagen de más arriba a la derecha. Obviamente los nombres con números de las carpetas se generan automática y azarosamente. La segunda carpeta tiene una copia del ensamblado generado por la compilación del proyecto, ese de la carpeta bin y un archivo ini con información adicional. Es importante notar que hay una cantidad importante de archivos y de diversos tipos, los que pasamos a revisar a continuación:




  • aluvtjbz.0.cs: Archivo de código c# generado a partir del archivo global.asax.


  • aluvtjbz.cmdline: archivo con las instrucciones de compilación del archivo interior. Muy interesante.


  • aluvtjbz.dll: ensamblado con el resultado de la compilación del archivo aluvtjbz.0.cs


  • aluvtjbz.err: en mi caso, vacío. Podría deducir que se almacenan lo errores de compilación.


  • aluvtjbz.out: Salida generada por pantalla cuando se compiló el archivo aluvtjbz.0.cs, producto de la ejecución del comando escrito en aluvtjbz.cmdline.


  • aluvtjbz.pdb: Símbolos asociados al ensamblado generado en la compilación.

Notar que cuando digo el archivo de código generado, no me refiero al archivo global.asax.cs  sino que al archivo que se genera para la compilación del archivo global.asax (sin .cs) propiamente tal. Complementariamente, los mismos archivos existen para el archivo WebForm1.aspx, almacenados en los archivos de nombre amyrvbql.


Adicionalmente hay algunos archivos más, que almacenan información de dependencias de archivos. Estos son:




  • hash.web que almacena algún hash identificador del sitio web


  • global.asax.xml, almacena las dependencias de global.asax (el mismo archivo global.asax y el ensamblado de la carpeta bin)


  • de1daaf3.web información al menos desconocida para mi (archivo de tamaño 0)


  • WebForm1.aspx.de1daaf3.xml dependencias de WebForm1.aspx (contiene una referencia a el mismo archivo WebForm1.aspx y al ensamblado de la carpeta bin)

Después del primer requerimiento se han compilado los archivos requeridos para servir el contenido necesario. Como se solicitó el archivo WebForm1.aspx, fue generado éste y el global.asax, utilizado siempre en cada requerimiento.





Ésta compilación es la que es modificada de acuerdo al valor de la configuración. En inglés lo podrán encontrar como Batch Compilation.
 

Antes de ver qué sucede si cambiamos la configuración a debug=”false”, agregaremos algunas páginas más a la aplicación, y otras carpetas con más páginas. Además, he agregado un par de controles de usuario en cada carpeta, los cuales son usados por los archivos WebForm1.aspx y WebForm4.aspx.


 








Después de limpiar la carpeta temporal y acceder  WebForm1.aspx (la que tiene el control de usuario), el contenido de la nueva carpeta temporal es:



 


También se han generado archivos de dependencias para todos los archivos compilados, con nombres similares a los anteriores. Si se fijan además, no se ha compilado nada de WebForm2.aspx ni ninguno de los archivos de la carpeta newfolder1.







Archivos en otras carpetas


Si ahora accedo a los archivos WebForm3.aspx y WebForm4.aspx, considerando que esta última tiene incrustado el control de usuario WebUserControl2.ascx, ocurre que ahora tengo muchos archivos en esta carpeta, los cuales han sido compilados cada uno en un ensamblado diferente. Pueden probar abriéndolos con [Reflector], como se muestra en la imagen de arriba a la derecha, para el archivo nmxwjakq.dll.


Cambiemos la configuración


Al cambiar la configuración a debug=”false” y al hacer el primer requerimiento a la página WebForm1.aspx, nuestro directorio se limpia automáticamente y se generan nuevos archivos, pero esta vez son muchos menos.







Además del hecho de que son menos archivos y con optimizaciones (que veremos en otra oportunidad), aspnet ha compilado todos los archivos del directorio, independientemente si eran o no requeridos para servir la página WebForm1.aspx.

 

Una peculiaridad del resultado de éste proceso es ver cómo se han agrupado los archivos para la compilación. Si recordamos, el archivo WebForm1.aspx tiene una instancia del control WebUserControl1.ascx pero no así el archivo WebForm2.aspx. Sin embargo, si vieron con detenimiento la imagen donde se mostraba la vista desde Reflector, habrán notado que el compilado de WebForm1.aspx va en un ensamblado (fqv6no_s.dll) y todo el resto de archivos aspx y ascx (menos  global.asax) se han compilado en otro ensamblado (uwsjh005.dll). El archivo global.asax se compiló en m9mdpgdl.dll. Agregué un archivo WebForm5.aspx sólo para comprobar que agregaba todos los archivos restantes.


Notemos también que no se han compilado los archivos de la carpeta NewFolder1. No obstante, al acceder WebForm3.aspx (que no contiene una instancia del control WebUserControl2.ascx, se compilan todos los archivos de esa carpeta, en 2 ensamblados. Uno de los ensamblados contiene a WebForm3.aspx y WebUserControl2.ascx, y el otro ensamblado a WebForm4.aspx. Mostraremos más abajo una imagen desde Reflector.


No estoy seguro de porque los agrupa así, pero me inclino con casi total seguridad que se debe a las dependencias. Para poder compilar un archivo que contiene un control incrustado, necesitará primero compilar el control y una vez que éste fue compilado, compilar la página que lo contiene.


Referencias a otras carpetas


¿Qué sucede si agregamos en WebForm1.aspx (que ya contiene una referencia a WebUserControl1.ascx) una referencia a WebUserControl2.ascx?


El resultado obtenido es el esperado aunque con una pequeña sorpresa. Lo esperado es que se compilan todos los archivos ya que el requerimiento de la página la WebForm1.aspx hace que se compile toda la carpeta donde está este archivo, como también que se compilen todos los archivos de la carpeta donde está WebUserControl2.ascx, el cual ahora era utilizado desde WebForm1.aspx. La sorpresa, al menos para mí, vino del hecho de que se utiliza el mismo patrón de agrupación de archivos para la compilación.


Si se fijan en las imágenes de más abajo, a la izquierda está el resultado de requerimientos por separado para cada WebForm1.aspx y WebForm3.aspx, ocurrida en dos momentos diferentes. La de la derecha, sólo para el llamado de WebForm1.aspx. Las agrupaciones de archivos son equivalentes.






 

Por último, ¿qué sucede si ambas páginas, o mejor dicho, los tres formularios de la raíz (WebForm1.aspx, WebForm2.aspx y WebForm5.aspx) tienen incrustado una instancia de WebUserControl1.aspx?







Antes de hacer la prueba, voy a dar mi vaticinio. Ustedes podrán confiar o no de que estoy dando mi vaticinio sin haber el resultado [;)], en el caso de apuntarle correctamente. Creo que pueden pasar 2 cosas. O bien se compilan todos pos separado o se compilan los aspx todos juntos (ya que no tienen dependencias entre ellos). Mi apuesta va por la segunda.


Mi intuición fue acertada. Efectivamente agrupó a todos los WebForm*.aspx en un solo ensamblado, el WebUserControl1.ascx en otro, y por último, el global.asax en otro. La imagen de Reflector nos muesta el resultado a la derecha.



Para la segunda parte, dejaremos el estudio de las opciones disponibles en el ítem de compilación en el archivo web.config, como también las conclusiones de porque no debo definir el atributo debug=”true” en producción. Aún nos falta ver qué sucede con las modificaciones a los archivos y las re-compilaciones.


Diferencias con Visual Studio 2005


Como ya había mencionado, en Visual Studio 2005, la forma de compilación y entrega para proyectos web cambió considerablemente. Sin entrar en justificaciones como tampoco en detalles, sólo agregaré que Microsoft enmendó el rumbo proveyendo algo similar a como funcionaba en Visual Studio 2003. Éste se llama Visual Studio 2005 Web Application Projects, disponible en la dirección http://msdn2.microsoft.com/es-cl/asp.net/aa336618.aspx. Si ya instalaste Service Pack 1, ya lo tienes.


Si te interesa además saber todos los detalles sobre la compilación en Visual Studio 2005 y formas de hacer entregas (deployment) te recomiendo el siguiente artículo de Rick Strahl, llamado Compilation and Deployment in ASP.NET 2.0 disponible en http://www.code-magazine.com/Article.aspx?quickid=0609061


Tareas para la casa


Prueben cambiando el lenguaje de compilación y vean que ocurre. Háganlo eso sí con debug=”true” para que vean los archivos de código generados.

Patrick.
Sao Paulo, Brasil.

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

La contradicción de las aplicaciones ultra parametrizables y customizables

Estas  últimas semanas han estado un poco aburridas. No se han presentado problemas de aplicaciones pero seguramente se presentarán… tarde o temprano. Sin embargo, a falta de problemas que se presentan, nos encargamos de buscarlos, o más bien dicho, de forzarlos.


Una de las últimas actividades realizadas corresponde a la revisión de una aplicación que será liberada pronto. Esta revisión consistió en generar pruebas de carga para diferentes casos de uso y analizar los resultados.

Actividad realizada

Grabamos los scripts con [ACT], los modificamos con [fiddler] y los ejecutamos con [ACT].

Resultados

Los resultados fueron sorprendentes, y no lo que se esperaba. La aplicación no lograba procesar más de 0,5 requerimientos por segundo, consumiendo el 70% de la CPU con un sólo usuario conectado.

Análisis detallado

La aplicación analizada pertenece a la fauna de aplicaciones que han sido concebidas con la idea de ser totalmente customizables y parametrizables. Particularmente no estoy en contra de las aplicaciones parametrizables, y más aún, el no permitir que se adapten a ciertas variables es una limitación, y producirá en el cortísimo plazo problemas de mantención.

¿Cuál es la arquitectura utilizada?

La aplicación está montada sobre un framework desarrollado por un tercero.
Nota aparte: ¿Se han fijado que ahora todo el mundo hace frameworks?, ¿y a casi cualquier cosa desarrollada le llaman framework? Bueno, volvamos a la aplicación.


Esta es una aplicación web desarrollada en .net montada con este framework. No puedo decir que es una aplicación ASP.NET ya que apenas utiliza ASP.NET.

¿Por qué no usa ASP.NET?

Cada página no tiene controles asp.net; ninguno. Así de simple. Incluso las páginas generadas no tienen viewstate, prueba de lo anterior.

¿Cómo es esto posible?

Las páginas son generadas desde una definición existente en algún archivo XML (400 kilobytes), el cual, en algún lugar indica con cierto criterio, donde buscar los controles que deben pintarse en la página, los que están definidos en una tabla en una base de datos de SQL Server.


Debido a lo anterior, no es de extrañarse que para pintar una página, recurra muchas veces al servidor de datos. Para complicar aún más el escenario, cada ítem de la página es un ítem en la base de datos. Todo esto para poder definir paramétricamente las páginas. Notar al final las contradicciones de este modelo.


Más aún, la lectura desde el servidor SQL es procesada en largas funciones y ciclos, los cuales, para más males, concatenan string en algunos casos. El resultado de esta gran concatenación es, como alguno de ustedes podrá adivinar/suponer, XML.


Luego, este XML es transformado en conjunto con un XSL que define la salida de la página. Recordar que el requerimiento era que la aplicación fuese mantenible fácilmente, algo que ya no se cumple.


Ok, después de todo este trabajo, la página está pintada.

¿Qué pasa si hago clic sobre algún botón?

Cómo no existen los controles de servidor, no existe un evento asociado al botón, y la página se procesa como se hacía en ASP, es decir, con request.form[] y sus amigos.


Lo bueno al menos es que como todo esto lo maneja este framework, es él el encargado de procesar el evento y llamar a las funciones de negocio asociadas.


Las funciones de negocio se definen en un XML, de varios cientos de kilobytes. En este archivo, utilizando información súper rígida, se define la ruta física del assembly, además del tipo y la función que se llamará. No es necesario definir los parámetros de entrada, porque se usa un único parámetro de tipo string, que en el fondo, es un XML.


Si en las funciones de negocio se requiere llamar a un procedimiento almacenados (SP), hay que hacerlo a través del framework. Obviamente hay que construir un XML con la información del SP y los parámetros. Sin embargo, en el XML no se escribe el nombre del SP sino que un pseudo-nombre asociado a una pseudo-base de datos. Entonces, el framework internamente se encarga de ir a buscar a otro XML, y que tiene como nombre el mismo valor de la pseudo-base de datos, la definición real del SP.

Contradicciones vitales

El sistema fue concebido así para poder ser fácilmente adaptable sin necesidad de mayores cambios en el código. La contradicción está en que el único que es capaz de hacer algún cambio en la aplicación es alguien que domine todos los elementos mencionados hasta ahora.


Es interesante que no se esté utilizando el GAC. ¿Será porque los assemblies cambian mucho en el tiempo y existe un costo administrativo de firmarlos e incluirlos en el GAC? Si cambian mucho en el tiempo, ¿Cuál era el problema de hacer una aplicación más rígida, o dicho de otra forma, una aplicación normal utilizando controles ASP.NET y librerías de clases?

Otra información

¿Si se preguntan cuántas veces baja a la base de datos para pintar una página especifica?
Más de 400


Saludos,
Patrick

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

Concatenación de strings y como “matar” un servidor

Uno de los problemas que usualmente uno enfrenta es el alto uso de CPU de un servidor y la “poca” capacidad de procesamiento de éste.


La forma tradicional de analizar estos problemas de alto uso de CPU es tomar dumps de memoria mientras la CPU esta con alto uso y ver que está ejecutando cada thread en el momento de la “foto”.


Para el caso que revisé hace un tiempo, en un sitio de muchas páginas, el dump tomado mostraba que una de ellas estaba consumiendo mucha CPU. Después de analizar con más detalle que actividad estaba realizando ésta, como podrán deducir del título del post, estaba concatenando strings para generar el output.


Los desarrolladores novatos, y los no tanto también aunque en menor medida, tienden a utilizar bastante la concatenación de strings para armar una salida de html o xml. El problema radica en que no conocen o subestiman el “poder de destrucción” de este proceso, en recursos como la CPU y [memoria].


Revisemos este código que utilizaré para la demo de esta oportunidad. En ésta, he sobre-simplificado una página asp y aspx (el problema es el mismo) que concatena string para armar una salida html y escribirla en un label de aspnet, o con response.write.





String sb = string.Empty;
for (int i=1 ; i<=1000;i++)
{
       sb = sb + “dsfih      yhfuwefifiojfdshf dhfdh dhf fhiua hfdoisfog h hiuh asdfgdsfhiufhds<br>”;
}
this.Label1.Text = sb;

El problema de concatenar

El gran costo de la concatenación de string se debe a la inmutabilidad del tipo (¿inmutabilidad?). Bueno, esa es la definición técnica correcta. Pero llevándolo a un lenguaje más simple, el problema radica en que los strings no se pueden alterar y cada vez que se concatena, se deben generar nuevos strings.


¿Aún no está claro?… veamos con un ejemplo.


Supongamos que tenemos este código (como dice [Robbins], un código vale más que mil palabras):





String test = string.Empty;
test = “primera línea”;                        Linea 1
test = test + “<br>”;                          Linea 2
test = test + “segunda línea”;                 Linea 3
test = test + “<br>” + “tercera línea”;        Linea 4
test = test + “<br>cuarta linea”;              Linea 5

¿Cómo afecta la inmutabilidad del string en este proceso?

Al ejecutarse la línea 1, se piden 13 bytes para almacenar el string “primera línea”. Realmente son más que 13 pero esto no influye en lo que se quiere explicar.


Al ejecutarse la línea 2, se calcula el largo de test y el largo del texto a concatenar. En este caso son 13 bytes y 4 bytes, y se piden 17 bytes. Luego, los 13 bytes son copiados desde la primera variable y luego son copiados los 4 bytes de la segunda variable. Una vez copiados todos los bytes, se asigna el string resultante a la variable test.


En este momento hemos comprobado la inmutabilidad del string. No puedo simplemente concatenarle “<br>” a final de test. El CLR (y la máquina virtual de VB 6.0 también) deben crear nuevos strings y copiar todo el contenido.


La línea 3 es similar a la anterior. Los largos actuales son 17 y 13 bytes respectivamente. Se debe pedir 30 bytes, copiar los 17, luego los 13 y asignar el nuevo valor a test.


La línea 4 funciona mejor de lo que esperamos. El funcionamiento está optimizado y en vez de tratarse “<br>”+”tercera línea” como dos strings, para la concatenación se consideran como si fuese uno sólo, requiriendo los 30 bytes de test, más los 17 de “<br>”+”tercera línea”. El proceso continua igual, copiando y asignando.


La línea 5 es equivalente a la 4, salvo que no son 17 bytes sino 16.


Bueno, alguien podría decir que un proceso tan simple como éste no debiera afectar el rendimiento de un servidor con muchas CPUs y memoria. En efecto, en este ejemplo básico, el costo de la concatenación es despreciable. El problema real ocurre cuando esta está dentro de un ciclo y la variable test almacena varios miles de bytes.


Ya no es tan rápido pedirle 35 kilobytes, luego copiar los 35 kilobytes de un string a otro y luego copiar lo que se quiere concatenar.


Personalmente he visto en dumps un string de 450 Kilobytes siendo concatenado. Ni les digo como estaba la CPU de ese servidor.

Solución en .net

Para evitar este problema, en .net existe una clase llamada StringBuilder, disponible en System.Text. StringBuilder. La “gracia” de ésta es que permite concatenar efectivamente strings sin crear nuevos strings por debajo.


Definición y ejemplos de uso, disponibles en español en el siguiente link:


http://msdn2.microsoft.com/es-es/library/system.text.stringbuilder(VS.80).aspx

Bueno, mucha habladuría y poca demostración. Vamos a ver cómo se comporta mi demo.

He creado tres páginas aspx, con el mismo texto en page_load, salvo que en una de ellas, el desarrollador ha decidido utilizar concatenación de strings en vez de seguir el ejemplo de los otros desarrolladores que habían utilizado correctamente StringBuilder.


Implementación correcta, en páginas webform1.aspx y webform2.aspx





private void Page_Load(object sender, System.EventArgs e)
{
       System.Text.StringBuilder sb = new  System.Text.StringBuilder();
       for (int i=1 ; i<=1000;i++)
       {
             sb.Append(“dsfih      yhfuwefifiojfdshf dhfdh dhf fhiua hfdoisfog h hiuh asdfgdsfhiufhds<br>”);
       }
       this.Label1.Text = sb.ToString();
}
 

Implementación Incorrecta, en página webform3.aspx





private void Page_Load(object sender, System.EventArgs e)
{
       String sb = string.Empty;
       for (int i=1 ; i<=1000;i++)
       {
             sb = sb + “dsfih      yhfuwefifiojfdshf dhfdh dhf fhiua hfdoisfog h hiuh asdfgdsfhiufhds<br>”;
      
}
       this.Label1.Text = sb;

}

 

Adicionalmente creé una página llamada default.aspx que me permitía llegar a estas tres páginas.





<TABLE id=”Table1″ cellSpacing=”0″ cellPadding=”0″ width=”300″ border=”0″>
       <TR><TD><a href=”webform1.aspx”>Pagina 1</a></TD></TR>
       <TR><TD><a href=”webform2.aspx”>Pagina 2</a></TD></TR>
       <TR><TD><a href=”webform3.aspx”>Pagina 3</a></TD></TR>
</TABLE>


Luego grabé con ACT un script para una prueba de carga con la ejecución de las páginas. Para demostrar con mayor fuerza el impacto de la concatenación, en  el script decidí incluir cinco ejecuciones de la página webform1.aspx (correcta), cuatro de la página webform2.aspx (correcta) y una sola de la página webform3.aspx (incorrecta).


Mi interés no es ensañarme con la página webform3.aspx sino que demostrar que con el 10% de las páginas concatenado string, el impacto es severo.

Prueba de carga

La prueba de carga la realicé durante un minuto y con 5 usuarios concurrentes, lo suficiente para entretener al servidor. Posteriormente corregí el código reemplazando la concatenación de string por un stringbuilder y ejecuté el mismo script grabado.


Estos son los resultados obtenidos.


La ejecución con concatenación de strings corresponde a la con el número (1) de color celeste. La con stringbuilder corresponde a la con número (2) de color café oscuro.


Reporte

Conclusión

El gráfico y las tablas son totalmente concluyentes. Concatenando strings logramos ejecutar sólo 4.901 requerimientos de páginas, pero con la versión con stringbuilder, se llega hasta 20.903 requerimientos. La versión corregida permite ejecutar 4,26 veces la cantidad de requerimientos o dicho de otra forma, sólo podemos obtener el 23,4% de rendimiento desde nuestro servidor con una página concatenando string. Por supuesto, estos valores son relativos al sistema operativo, versión de IIS, cantidad de CPUs, memoria, etc. Esto es sólo un ejemplo ejecutado en un computador de escritorio.


Por otra parte, es necesario explicar que en la prueba donde se concatena strings, el resultado se ve afectado por dos motivos. Uno de ellos ya ha sido mencionado y corresponde al hecho de pedir memoria y copiar el contenido. El otro motivo es el alto tiempo que el garbage collector (GC) pasa trabajando limpiando los strings que ya no se usan. El uso (y abuso) del GC lo veremos en otra oportunidad.


Saludos,
Patrick

Minimizar el impacto de subidas a producción (waitChangeNotification, maxWaitChangeNotification)

A pesar de que no es recomendado, muchas veces nos hemos visto forzados a subir cambios a producción “en caliente.”


Estos cambios en caliente generalmente se deben por cambios en machine.config, web.config o el copiado de nuevos assemblies a la carpeta bin, y estos cambios obligan a que se reinicie el dominio  de la aplicación asp.net.


Como dominio de aplicación entendemos una estructura interna en memoria, dentro de un proceso, donde están cargados los assemblies, las páginas compiladas y toda la memoria utilizada en el funcionamiento de nuestra aplicación. Esta explicación es muy pobre, pero contiene lo necesario para entender el costo de los cambios.


Cada vez que se inicia o carga un dominio, previamente se debe descargar, esto es, suponiendo que no es la primera vez que se inicia. Y antes de poder descargarse, debe terminar de procesar todos los requerimientos que tiene activos.


Debido a lo anterior, cada vez que copiamos assemblies a la carpeta bin, disparamos una descarga del dominio. Si en ese momento entra un nuevo requerimiento, se crea un nuevo dominio para atender el requerimiento (con el o los nuevos assemblies que hayan disponibles producto de la copia). Sucesivas copias en el mismo período de tiempo hará que la situación se repita. Esto último puede llevar a nuestro servidor y aplicación a quedar en un estado inestable.


Personalmente lo he vivido varias veces, cuando se sube en caliente para corregir bugs críticos que no pueden esperar. Dentro de los errores recibidos producto de estas subidas extremas, estaba un extraño error de compilación de las páginas. Generalmente, la única manera de resolver la inestabilidad era haciendo un iisreset.


Si hacemos un resumen de cuánto tiempo perdíamos en esas subidas, generalmente era entre 30 y 45 segundos. No vamos a hablar ahora de las molestias a los clientes. Primero por una aplicación que respondía inestablemente y luego por que definitivamente no prestábamos servicio.


Para minimizar este problema y “garantizar” que el impacto de una subida a producción sea mínimo, existen un par de opciones disponibles para configurar en el archivo web.config. Por favor, notar que utilicé comillas en garantizar. Esto debido a que si se configuran mal las opciones, el resultado no es el mejor (aunque mejor que el escenario anterior sin la configuración).


Estas opciones son waitChangeNotification y maxWaitChangeNotification. La documentación oficial de estos contadores está en http://msdn2.microsoft.com/es-es/library/e1f13641(VS.80).aspx, pero la calidad de la explicación es bastante pobre.

Vamos a hacer un mejor esfuerzo.

Aclaremos primero que ambas almacenan valores en segundos y a pesar de que la documentación dice que es un atributo sólo de aspnet 2.0, en la versión 1.1 también funciona (al menos en mi XP Pro con IIS 5.1, y la aplicación web configurada en 1.1)


La primera opción de configuración (waitChangeNotification) instruye al IIS y aspnet cuanto tiempo debe esperar a que un nuevo requerimiento genere un nuevo dominio una vez que ha detectado un cambio.


Esto nos ayuda a darnos un rango de tiempo para poder copiar todos los archivos necesarios. Este rango de tiempo es desplazable, lo que significa que son X segundos después de la última notificación de cambio (último archivo subido/modificado/agregado), descartando todas las notificaciones anteriores.


La segunda opción (maxWaitChangeNotification) establece el tiempo máximo en que se reiniciará el dominio. Esta opción existe ya que como la anterior es desplazable, sucesivos cambios en un breve tiempo podrían demorar el reinicio del dominio más de lo esperable.


Veamos un ejemplo para aclarar esto.

Ejemplo

Configurando el primer atributo en 5 segundos, si realizo 10 modificaciones en total, cada una separada 4 segundos de la modificación anterior, el reinicio se realizaría a los 41 segundos, sólo 5 segundos después de la última modificación. Esta última ocurre a los 36 segundos ya que la primera ocurre a los 0 segundos (0-4-8-12-16-20-24-28-32-36).


La segunda opción de configuración, si se define en 30 segundos, implicaría que el escenario anterior nunca ocurra ya que habría forzado el reinicio a los 30 segundos, entre la 8va y 9na modificación.


Con esta segunda opción, el segundo reinicio sería a los 11 segundos después, suponiendo que el reinicio es instantáneo.

De vuelta a la realidad

A pesar de que el ejemplo anterior puede parecer complejo, no dejes que éste te confunda. Lo importante es darle un tiempo a tu aplicación para que se reinicie limpiamente sin afectar demasiado a los clientes. Elije un tiempo acorde a las necesidades de tu aplicación y configura las dos opciones, no sólo la primera, y la segunda debe ser mayor a la primera.


¿Cómo se modifica en el web.config?
<system.web>
<
httpRuntime waitChangeNotification=X maxWaitChangeNotification=Y/>
</system.web>


Saludos,
Patrick

Variables de sesión y costos escondidos

Hace algunas semanas estuve de visita en un cliente, en donde me encontré con una aplicación que cada cierto tiempo, experimentaba excepciones de escasez de memoria (Out Of Memory).


Como vimos en el post sobre la analogía entre la memoria de un servidor y un restaurant,  http://msmvps.com/blogs/pmackay/archive/2007/02/02/netadmin.aspx, una de las causas por las que se producen las excepciones por falta de memoria, es por la no liberación de los objetos, entre otras.


Después de tomar unos dumps de la memoria del proceso y realizar los análisis pertinentes, encontré gran cantidad de la memoria referenciada en variables de sesión, con variables de tamaño superior a algunos megabytes en algunos casos.

Variables de sesión y el pipeline http de aspnet

Es sabido que no se debe almacenar objetos de gran tamaño en las variables de sesión debido al costo que implica su acceso. Recordemos que cada vez que el servidor recibe un requerimiento, éste es llevado a través de todo el pipeline http de ASP.NET  y los módulos que están habilitados en él hasta llegar al handler que procesará finalmente el requerimiento. A su vez, cuando el handler termina de procesar el requerimiento, la respuesta debe traspasar el mismo pipeline en sentido opuesto, y una vez terminado el proceso, ésta es enviada al cliente por el servidor.


 


Módulos y Handlers aspnet


 


Uno de estos módulos es el de las variables de sesión. Si está habilitado, como ocurre por defecto, ASP.NET carga las variables de sesión desde el repositorio (InProc, StateServer o SQL Server) cada vez que se hace un requerimiento a una página, independientemente si se van a usar o no, y las almacena de vuelta en el repositorio al descargar la página. Técnicamente hablando, en el caso de InProc no “carga” nada porque estas ya están cargadas en el proceso.


Para tener un control más fino del uso de variables de sesión en una página o en un sitio completo, se puede utilizar la propiedad de la página llamada EnableSessionState (http://msdn2.microsoft.com/en-us/library/ms178581.aspx).


Esta propiedad permite habilitar el acceso a las variables de sesión en dos modalidades. Las modalidades disponibles son lectura solamente y lectura y escritura. También es posible deshabilitar el acceso a las variables de sesión, liberando de realizar trabajo al servidor cada vez que vaya a procesarse esa página.

Volvamos al caso

Volviendo al caso que les mencionaba. Por motivos de confidencialidad no puedo exponer la información que obtuve en el caso, pero podemos repetirlo de forma muy simple.


Al revisar el dump obtenido del proceso aspnet_wp.exe, el listado los objetos contenedores de las variables de sesión es el siguiente.

Address         MT           Size  Gen
0x0106fad0      0x03749abc       48    2     System.Web.SessionState.InProcSessionState
total 1 objects

sizeof(0x106fad0) = 1,442,100 (0x160134) bytes (System.Web.SessionState.InProcSessionState)


Se puede ver fácilmente que el objeto InProcSessionState tiene un peso de 1,44 MB, que se encuentra en la generación 2 (más antigua) y que existe solo un objeto de este tipo, lo cual es correcto ya que estoy sólo en mi máquina haciendo las pruebas. Extraño sería ver más de 1.


Al analizar internamente el objeto InProcSessionState, podremos ver el contenido y ver que causa su gran tamaño.


Name: System.Web.SessionState.InProcSessionState
MethodTable 0x03749abc
EEClass 0x037599c0Size 48(0x30) bytes
GC Generation: 2
mdToken: 0x02000131  (c:\windows\assembly\gac\system.web\1.0.5000.0__b03f5f7f11d50a3a\system.web.dll)
FieldDesc*: 0x03749a10
        MT      Field     Offset                 Type       Attr      Value Name
0x03749abc 0x40009ee      0x4                CLASS   instance 0x00feb4c0 dict
0x03749abc 0x40009ef      0x8                CLASS   instance 0x00000000 staticObjects
0x03749abc 0x40009f0      0xc         System.Int32   instance 20 timeout
0x03749abc 0x40009f1     0x18       System.Boolean   instance 0 isCookieless
0x03749abc 0x40009f2     0x10         System.Int32   instance 0 streamLength
0x03749abc 0x40009f3     0x19       System.Boolean   instance 0 locked
0x03749abc 0x40009f4     0x1c            VALUETYPE   instance start at 0106faec utcLockDate
0x03749abc 0x40009f5     0x14         System.Int32   instance 2 lockCookie
0x03749abc 0x40009f6     0x24            VALUETYPE   instance start at 0106faf4 spinLock


Este objeto tiene una instancia de SessionDictionary en una variable llamada dict. El contenido de ésta es:

Name: System.Web.SessionState.SessionDictionary
MethodTable 0x03749614EEClass 0x0375963c
Size 44(0x2c) bytes
GC Generation: 2
mdToken: 0x0200013a  (c:\windows\assembly\gac\system.web\1.0.5000.0__b03f5f7f11d50a3a\system.web.dll)
FieldDesc*: 0x037494f0
        MT      Field     Offset                 Type       Attr      Value Name
0x031ab170 0x4000a8b     0x24       System.Boolean   instance 0 _readOnly
0x031ab170 0x4000a8c      0x4                CLASS   instance 0x00feb6c0 _entriesArray
0x031ab170 0x4000a8d      0x8                CLASS   instance 0x00feb6a8 _hashProvider
0x031ab170 0x4000a8e      0xc                CLASS   instance 0x00feb6b4 _comparer
 

Continuando la búsqueda, revisemos que contiene la variable interna _entriesArray, donde está cada variable de sesión de un usuario.

Name: System.Collections.ArrayListMethodTable 0x79ba0d74
EEClass 0x79ba0eb0Size 24(0x18) bytesGC Generation: 2
mdToken: 0x020000ff  (c:\windows\microsoft.net\framework\v1.1.4322\mscorlib.dll)
FieldDesc*: 0x79ba0f14
        MT      Field     Offset                 Type       Attr      Value Name
0x79ba0d74 0x400035b      0x4                CLASS   instance 0x00feb6d8 _items
0x79ba0d74 0x400035c      0xc         System.Int32   instance 1 _size
0x79ba0d74 0x400035d     0x10         System.Int32   instance 1 _version
0x79ba0d74 0x400035e      0x8                CLASS   instance 0x00000000 _syncRoot

_items contiene:

Name: System.Object[]MethodTable 0x00c3209cEEClass 0x00c32018
Size 80(0x50) bytes
GC Generation: 2
Array: Rank 1, Type CLASS
Element Type: System.Object
Content: 16 items
—— Will only dump out valid managed objects —-
   Address           MT   Class Name
0x00fec2a0   0x031ab5bc   System.Collections.Specialized.NameObjectCollectionBase/NameObjectEntry

Y el valor del objeto en el arreglo es:

Name: System.Collections.Specialized.NameObjectCollectionBase/NameObjectEntry
MethodTable 0x031ab5bc
EEClass 0x02fbb5c8
Size 16(0x10) bytes
GC Generation: 2
mdToken: 0x0200017a  (c:\windows\assembly\gac\system\1.0.5000.0__b77a5c561934e089\system.dll)
FieldDesc*: 0x031ab578
        MT      Field     Offset                 Type       Attr      Value Name
0x031ab5bc 0x4000a94      0x4                CLASS   instance 0x00fec224 Key
0x031ab5bc 0x4000a95      0x8                CLASS   instance 0x01074740 Value
 

Como bien sabemos, las variables de sesión se almacenan utilizando un identificador, en este caso, el key, y el valor asociado a éste.


Para ver el valor del identificador y el valor de la variable de sesión, vemos el contenido de ambos objetos, acompañado del código fuente de la página.

Name: System.StringMethodTable 0x79b925c8
EEClass 0x79b92914
Size 48(0x30) bytes
GC Generation: 2
mdToken: 0x0200000f  (c:\windows\microsoft.net\framework\v1.1.4322\mscorlib.dll)
String: pruebavariable
FieldDesc*: 0x79b92978
private void Button1_Click(object sender, System.EventArgs e)
{
       this.Session[“pruebavariable”]= Button1;
}
Name: System.Web.UI.WebControls.ButtonMethodTable 0x03746800
EEClass 0x0373f054
Size 96(0x60) bytes
GC Generation: 2
mdToken: 0x02000203  (c:\windows\assembly\gac\system.web\1.0.5000.0__b03f5f7f11d50a3a\system.web.dll)
FieldDesc*: 0x03746670
        MT      Field     Offset                 Type       Attr      Value Name
0x032f590c 0x4000b3a      0x4                CLASS   instance 0x00000000 _dataBindings
0x032f590c 0x4000b3b      0x8                CLASS   instance 0x00febb9c _id
0x032f590c 0x4000b3c      0xc                CLASS   instance 0x00febb9c _cachedUniqueID
0x032f590c 0x4000b3d     0x10                CLASS   instance 0x010744e8 _parent
0x032f590c 0x4000b3e     0x14                CLASS   instance 0x00000000 _site
0x032f590c 0x4000b3f     0x18                CLASS   instance 0x01080d58 _events
0x032f590c 0x4000b40     0x1c                CLASS   instance 0x00000000 _controls
0x032f590c 0x4000b41     0x38         System.Int32   instance 5 _controlState
0x032f590c 0x4000b42     0x20                CLASS   instance 0x00000000 _renderMethod
0x032f590c 0x4000b43     0x24                CLASS   instance 0x010747a0 _viewState
0x032f590c 0x4000b44     0x28                CLASS   instance 0x00000000 _controlsViewState
0x032f590c 0x4000b45     0x2c                CLASS   instance 0x00000000 _namedControls
0x032f590c 0x4000b46     0x3c         System.Int32   instance 0 _namedControlsID
0x032f590c 0x4000b47     0x30                CLASS   instance 0x01073acc _namingContainer
0x032f590c 0x4000b48     0x34                CLASS   instance 0x01073acc _page
…cortado para abreviar  

El objeto almacenado en sesión es un botón de web, del tipo System.Web.UI.WebControls.Button. Veamos el tamaño del objeto almacenado.

sizeof(0x1074740) = 1,442,148 (0x160164) bytes (System.Web.UI.WebControls.Button)  

¿Impresionante, no?, ¿Cómo es posible que un botón tenga un tamaño de 1,44 MB?


El tamaño real del botón, o lo que podemos entender como un simple botón, no es 1,44 MB. El problema se presenta ya que al momento de almacenar éste en una variable de sesión, en modalidad InProc , se agrega una referencia del objeto al arreglo interno utilizado para almacenar las variables. La referencia del objeto incluye también todos los objetos que sobre los cuales este tiene una referencia. Entre estos objetos podemos incluir la página misma, el viewstate e incluso el HTTPRuntime.  Existe otro problema con almacenar este tipo de objetos en sesión, el cual ya lo abordaremos en otra oportunidad.


Para confirmar lo anterior, es cosa de seguir “navegando” los objetos a los que el botón hace referencia, actividad que queda para ustedes ya que escapa al objetivo de este post.


Name: System.Web.UI.WebControls.Button
MethodTable 0x03746800EEClass 0x0373f054
Size 96(0x60) bytes
GC Generation: 2
mdToken: 0x02000203  (c:\windows\assembly\gac\system.web\1.0.5000.0__b03f5f7f11d50a3a\system.web.dll)
FieldDesc*: 0x03746670
        MT      Field     Offset                 Type       Attr      Value Name
0x032f590c 0x4000b3a      0x4                CLASS   instance 0x00000000 _dataBindings
0x032f590c 0x4000b3b      0x8                CLASS   instance 0x00febb9c _id
0x032f590c 0x4000b3c      0xc                CLASS   instance 0x00febb9c _cachedUniqueID
0x032f590c 0x4000b3d     0x10                CLASS   instance 0x010744e8 _parent
0x032f590c 0x4000b3e     0x14                CLASS   instance 0x00000000 _site
0x032f590c 0x4000b3f     0x18                CLASS   instance 0x01080d58 _events
0x032f590c 0x4000b40     0x1c                CLASS   instance 0x00000000 _controls
0x032f590c 0x4000b41     0x38         System.Int32   instance 5 _controlState
0x032f590c 0x4000b42     0x20                CLASS   instance 0x00000000 _renderMethod
0x032f590c 0x4000b43     0x24                CLASS   instance 0x010747a0 _viewState
0x032f590c 0x4000b44     0x28                CLASS   instance 0x00000000 _controlsViewState
0x032f590c 0x4000b45     0x2c                CLASS   instance 0x00000000 _namedControls
0x032f590c 0x4000b46     0x3c         System.Int32   instance 0 _namedControlsID
0x032f590c 0x4000b47     0x30                CLASS   instance 0x01073acc _namingContainer
0x032f590c 0x4000b48     0x34                CLASS   instance 0x01073acc _page

cortado para abreviar


Conclusión


El uso de variables de sesión es conocido y utilizado mundialmente. A su vez, aplicaciones hacen un uso indiscriminado de éstas, muchas veces sin considerar ni reparar en lo que realmente están almacenando.


Para el caso presentado, no debe concluirse que el error está en cómo el objeto botón es almacenado en una variable de sesión, que para el caso de InProc, es sólo la referencia a éste. Tampoco podemos esperar que almacene solamente el botón y no “el resto”, ya que esa definición de “resto” no existe. Desde el punto de vista del CG (Garbage Collector), se almacena la referencia del objeto (en modalidad InProc) y por consecuencia, el grafo de objetos que parten de éste seguirán vivos. Si esto no ocurriese así, una vez que requiera obtenerse el objeto para volver a ser utilizado, el resultado de cualquier ejecución sobre él sería inesperado.


Es importante recalcar en este punto que el resultado de “pegar” este control generado en una página anterior sobre una nueva página, que aunque sea del mismo tipo, no es la misma instancia, podrá llevar a resultados impredecibles.


Al almacenar objetos en variables de sesión, como también en el caso del caché, variables de aplicación o cualquier “repositorio”, se debe tener extremo cuidado en conocer las implicancias de todo lo que se está almacenando y los otros objetos que se almacenan de forma imperceptible. Estos costos escondidos pueden degradar el rendimiento de nuestra aplicación, pudiendo ser necesario invertir muchos recursos para determinar la causa.


Para finalizar el post, ¿Qué ocurriría si se utilizan variables de sesión almacenadas fuera del proceso, como por ejemplo SQL Server o StateServer?


Recursos adicionales


Pipeline http de asp.net.


http://msdn.microsoft.com/msdnmag/issues/02/09/HTTPPipelines/default.aspx


http://msdn2.microsoft.com/en-us/library/aa479328.aspx


Saludos,
Patrick

Administración de la memoria en Windows y .NET

Quienes hemos desarrollado aplicaciones o hemos estado a cargo de la mantención de un sitio web, en alguna oportunidad nos topamos o seguramente lo haremos en el futuro, con una excepción del tipo “Out Of Memory Exception” o “OOM Exception”.


¿Por qué ocurren los Out Of Memory Exceptions?


Quien haya visto esto, se preguntará, ¿Cómo es posible que tenga 4 GB de memoria en el servidor y mi aplicación se cae por falta memoria cuando “sólo ha ocupado” 800 MB?


Ahora veremos someramente cómo funciona la administración de memoria en Windows y en .NET. No es mi intención cubrir el tema de forma extensa, en especial la administración de .net. Eso lo veremos en sucesivos posts.


Antes de comenzar, quiero hacer hincapié en que este post está basado en otro post de este blog, del cual incluso tomé “prestada” la imagen.


Utilizaremos una analogía para explicar el funcionamiento ya que será mucho más fácil de visualizar y entender. En este caso, la analogía corresponde a un restaurant donde las personas usualmente se juntan a comer en grupos.


¿Cómo se vería este restaurant antes de estar abierto al público, pero una vez que se haya instalado todo lo necesario para su funcionamiento?
Este es un estado “similar” al que correspondería al estado antes de empezar a ejecutar la aplicación. Por “similar” me refiero a que no consideraremos la memoria utilizada para cargar las dlls, utilizada por threads y otros elementos vitales en el funcionamiento de ésta. Sólo nos enfocaremos en la memoria que es pedida y liberada durante el funcionamiento


Este restaurant tendría todas sus mesas disponibles para los clientes que van entrando a comer.


¿Qué sucede cuando se pide memoria al sistema operativo?


Debido a que la solicitud de memoria es un proceso costoso, y suponiendo que se va a realizar miles o millones de veces durante la ejecución de la aplicación, el funcionamiento desde el punto de vista del sistema operativo es diferente a como usualmente lo podríamos suponer.


La memoria, desde el punto de vista del sistema operativo, tiene 3 estados posibles: libre (FREE), reservada (RESERVED) y comprometida (COMMITED).


Una aplicación, al necesitar memoria, le solicita al sistema operativo que le reserve (FREE->RESERVED) un espacio (a veces pequeños, otras veces de varios MBs), para posteriormente manejar élla las miles de solicitudes (RESERVED->COMMITED) y liberaciones (COMMITED->RESERVED) que ocurren a cada segundo. Es decir, el sistema operativo le entrega un pedazo de memoria y la aplicación lo administra. Cuando no le queda espacio libre, le pide al sistema operativo que le reserve otro espacio y el proceso continúa.


La memoria reservada es lo que se conoce con el nombre de VIRTUAL BYTES, y la memoria comprometida, PRIVATE BYTES. Hasta Windows 2003 Server (incluido), ninguno de los contadores de memoria del Task Manager representa estos valores. Solo es posible obtenerlos con performance monitor. Windows Vista si los incorpora, y seguramente Longhorn lo hará también.


Bueno, y ¿qué sucede con el restaurant?


La aplicación es el restaurant, y a  medida que llegan clientes, se va ocupando el espacio en él. Lo importante en este punto es saber cómo se llena.


Supongamos que llamamos al restaurant y decimos que vamos 3 personas. Rara vez, un restaurant tiene mesa para 3 personas. Usualmente son de números pares. Si existe una mesa para 4 personas, nos será reservada para que cuando lleguemos, la podamos utilizar. Lamentablemente para el restaurant, estará perdiendo dinero ya que no podrá utilizar ese espacio libre.


Después de unas horas de funcionamiento, nuestro restaurant se vería así. Todo el espacio blanco es la memoria disponible. Las sillas azules corresponden a las que están reservadas y las rojas a las comprometidas.



En la primera mesa, alguien vino a comer sólo pero tuvo que ocupar una mesa de a dos. Mal para el restaurant.


En la segunda mesa, no le fue tan mal al restaurant. Tuvo que asignar una mesa de a 6 a la que asistieron 4.


La tercera mesa, fue utilizada por 3 personas, hasta llegar al peor escenario para el restaurant. Llega 1 sola persona y le tiene que entregar una mesa de 4.


Por favor notar que la solicitud de las mesas no es bajo ningún criterio de forma secuencial como lo estoy mencionando hasta ahora. La llegada de personas (solicitud de memoria) ocurre en cualquier orden.


¿Dónde comienzan los problemas?


¿Qué sucede si llama una persona y pide una reservación para 4 personas? No existe ninguna mesa disponible para sentar a las cuatro personas.


Alguien podría decir que se puede sentar 1 persona en la primera mesa, 2 en la segunda y 1 en la tercera, pero seguramente los comensales querrán sentarse juntos, y con mucha razón!!. De la misma forma, las solicitudes de memoria se deben entregar en un bloque contiguo.


Otro escenario que no se da en la imagen, corresponde al hecho de suponer que por que en una mesa hay 3 espacios disponibles y en la mesa de al lado 1, se juntan las dos mesas y se les sienta a las 4 personas juntas. Lamentablemente, por cómo se maneja la memoria, no es posible reacomodar las mesas (espacios reservados) una vez que han sido asignadas, como tampoco mover las mesas del restaurant!!!


¿Qué sucede entonces? Suponiendo que no existe más espacio que reservar en el restaurant, al cliente se le dice que está lleno, lo que corresponde a un Out Of Memory.


El dueño del restauran puede estar muy triste por que dejó ir a cuatro clientes cuando potencialmente había espacio para 9 personas más.


El administrador de la aplicación puede estar muy molesto porque la aplicación arroja errores de Out Of memory, pero aún hay memoria en el servidor, o eso dice task manager (en otro post veremos qué es lo que realmente entregan los valores del task manager).


¿Cómo saber cuánta memoria se desperdica? La respuesta es VIRTUAL BYTES – PRIVATE BYTES, es decir, lo que he reservado menos lo que he ocupado.


¿Y desde el punto de vista de .net y el garbage collector (GC)?


El GC le solicita al sistema operativo espacios de 64 MB de memoria contigua, lo que correspondería a una gran mesa de muchos asientos. En este restaurant especial, las personas no se molestan por sentarse juntas en la misma mesa (curioso!!).


Cada vez que se crea un objeto, la memoria requerida por éste se obtiene desde después de la última persona sentada, y así sucesivamente hasta que se llena la mesa.


De vez en cuando, alguien (GC) revisa que las personas que han terminado de comer, efectivamente desalojen el lugar (se libera la memoria) y todos los otros comensales se desplazan para dejar más espacio al final de ésta. Otros argumentan que están esperando a alguien (strong reference) así que aunque ya terminaron, no se levantaran de la mesa (pero si se desplazan), y por último, algunos personajes de “mal temperamento” dirán, no me muevo de aquí (ni se desplazan) porque tengo asiento con ventana y la vista está muy hermosa hoy. Estos últimos son objetos que han sido referenciados desde fuera del código manejado (pinned object).


Para el último caso, los pinned objects, nadie puede moverse hacia delante de la mesa mientras ellos estén en ella. Esto produce espacios vacios en la mesa, entre ellos u los de adelante, lo que se conoce como fragmentación de memoria en .Net. Este tipo de objetos no se puede mover ya que en código no manejado, las posiciones en memoria jamás cambian, no asi en .net ya que cada desplazamiento de la mesa implica nuevas posiciones para los objetos.


Una vez que se llena la mesa, se le pide al sistema operativo otra mesa de 64 personas, ¿y si no hay disponible?..ya adivinaron no?, Out Of Memory Exception.


Debido a lo anterior, tenemos que ser muy cuidadosos al desarrollar código, haciendo dispose y liberando los recursos apenas dejen de utilizarse. Ya hablaremos de esto pronto.


Patrick.