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

Bug en .net, en PasswordDeriveBytes y RijndaelManaged

Hoy, desde Ciudad de México donde reviso otra aplicación, recibo buenas noticias.

 

Hace ya un tiempo postee la existencia de un bug en .net 1.1 y 2.0, referente a recursos no manejados utilizados por PasswordDeriveBytes y RijndaelManaged. Finalmente ayer obtuve respuesta y efectivamente el problema existe y que será corregido en un próximo release del CLR.

 


Es bueno saber que como usuarios somos escuchados y que nuestros requerimientos son atendidos.


 

Más información podrán encontrar en:

http://connect.microsoft.com/VisualStudio/feedback/ViewFeedback.aspx?FeedbackID=254236

 

 

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.