Se viene un momento histórico…

Es cosa de meses; ¿Cuántos?, ¿dos? ¿tres?, no sé, pero falta poco para un momento histórico. Más sitios corriendo sobre IIS que sobre ningún otro servidor web.


La información de Netcraft muestra para el mes de octubre lo siguiente.


Solo un 10% de diferencia entre sitios corriendo Apache e IIS. Considerando una tasa promedio de los últimos 3 meses, en donde IIS sube un 1,41% mensual y apache baja un 1,65% mensual, debiera invertirse la situación en un poco mas de tres meses. Google podría tener algo que decir también.

En tres meses más lo vemos [;)]

Los meses anteriores mostraron esta información, para agosto y septiembre respectivamente.



 

Saludos, desde Santiago de Chile
Patrick

Pantallas Azules, ¿hasta cuando?

Seguramente al leer el título de este post pensaste en que me voy a referir a las pantallas azules de Windows. Correcto. También puedes haber pensado que es un post para criticarlas. Bueno, eso no será así. Este post tiene como objetivo explicarlas.


Historia


Hace ya un par de años, leí un recorte en el típico mural de las empresas, que estaba escrito por alguien conocido por mí y un referente nacional en muchos aspectos.


El artículo escrito se llamaba “El software nuestro de cada día” y estaba escrito por José Miguel Piquer.


Jo y Microsoft


José Miguel, o “Jo” como le dicen los más cercanos, fue profesor mío en algunos cursos en la carrera de “Ingeniería Civil en Ciencias de la Computación e Informática,” en la Universidad de Chile.


Siempre un gusto ir a sus clases, magister, doctorado y varios títulos más que pueden leer en su página personal del Departamento de Ciencias de la Computación (DCC), le recuerdo como acérrimo detractor de Microsoft y Windows.


Artículo


Bueno, el inicio del artículo escrito por él, esboza la siguiente frase:


Hay que aceptar que resulta extremadamente frustrante enfrentarse a sistemas que se caen: desde las pantallas azules clásicas de Windows, pasando por los sitios webs bancarios que no responden, hasta las agendas y sus fatal exceptions que se están volviendo más norma que excepción.


Después, el artículo va realmente a lo importante que él quiere exponer, pero con ésta introducción cae lamentablemente en facilismos con una frase que no aporta mucho, más que exacerbar el cuento de “nunca acabar” de las pantallas azules.


El artículo está escrito en julio del 2005, diez años después del festín de pantallas azules que nos daban Windows 95, Windows 98, Windows 98 segunda edición y finalmente (por suerte!) Windows Millenium Edition.


En mis años de estudiante (93-99) nunca hubo un PC en la universidad (salvo el de la secretaria del DCC), así que dudo que él personalmente se haya enfrentado a un sistema de estos, aunque no es por ahí donde quiero llevar este post.


Profesor, todos esperamos mucho de usted. Por favor, no caiga en frases fáciles. No las necesita. Usted está lejos de ser un político.


En el año 2005, ya existía Windows 2003, sistema operativo que casi no presentaba pantallas azules. Más aún, en 4 años, no recuerdo haber visto una pantalla azul en Windows 2003, experiencia que seguramente tú compartes.


¿Son las pantallas azules exclusivas de Windows?


Casi. En los otros sistemas operativos no son necesariamente azules.


Pantallas azules de diferentes sistemas operativos


 Hagamos una revisión de las pantallas y los sistemas operativos.


Tenemos la clásica de Windows 9X.


¿Mac OS?, por supuesto que sí, aunque tienen una diferencia con las de Windows. Son más bonitas y mejor logradas, pero el mensaje no dice nada útil, y que el usuario no pueda intuir que tiene que hacer.







Bueno, no siempre son bien logradas, aunque el mensaje es de mayor utilidad que la pantalla anterior.



¿Linux?, claro que sí, en variadas distribuciones, aunque son llamadas Kernel Panic (un nombre más realista para el problema que analizaremos más adelante).





¿Quién esté libre de pecado…


…que lance la primera piedra?, o como dice mi amigo Luisón, el sol sale para todos, y la noche también.


Como ven, no hay sistemas operativos sin fallas, aunque Apple crea y venda lo contrario. Todos enfrentan los mismos problemas. Apple tiene una ventaja en este aspecto sobre Microsoft, la cual se minimizará en el tiempo, mientras se pueda instalar Windows Vista o XP sobre hardware homologado por Apple.


Además, es cosa de leer foros para darse cuenta de que Apple funciona bien con software Apple. Cuando empiezas a instalar software de terceros que no cuentan con buenos procesos de certificación, la situación se equipara.


Vamos a la teoría


Considerando la arquitectura de procesadores x86, en el año 1982, con el lanzamiento del 80286, se comienzan a dar los primeros pasos en lo que hoy se conoce como protected mode.


Protected mode, provee dentro de un conjunto de características, la existencia de cuatro niveles de privilegio, conocidos también como anillos (rings), numerados desde el 0 al 3.



El anillo 0 tiene acceso irrestricto a todo. El anillo 3 tiene accesos restringidos y acotados. El anillo 1 tiene más restricciones que el 0, pero menos que el 2 y menos que el 3 obviamente.


Anillos y Windows


Hoy en día, un sistema Windows 2003 ejecuta código en dos anillos. El 0 y el 3. También corre para XP y 2000. Tengo entendido que Vista utiliza el 0, 2 y 3, pero no lo puedo garantizar.


En el anillo 0 se ejecuta el Kernel de Windows, el cual realiza algunas de las actividades listadas a continuación. Administración de memoria, acceso al hardware a través del HAL, acceso a dispositivos utilizando drivers, priorización de actividades, plug and play, acceso a redes utilizando protocolos como TCP/IP o HTTP, etc.


En el anillo 3 se ejecutan las aplicaciones. Cada vez que una aplicación necesita de alguno de los elementos anteriormente mencionados, y que maneja el Kernel, necesita solicitárselo a éste ya que la aplicación no tiene permiso para acceder directamente.


Esta solicitud y cambio de contexto entre la aplicación y el Kernel tiene un costo, el cual fue tratado de minimizar en el pasado, pero las lecciones aprendidas fueron duras.


¿Por qué se producen?


Una pantalla azul en Windows se produce cuando se produce una falla en código que se ejecuta en el Kernel.


Si una aplicación se cae (anillo 3), nuestro amigo Dr. Watson la atiende y la mata, pero el sistema operativo sigue funcionando sin problema. Hoy en día, Windows te pregunta si deseas enviar el reporte de error a Microsoft, utilizando la aplicación Windows Error Reporting.


Si el Kernel falla (anillo 0), no hay remedio ni doctor que te salve. Bueno, Dr. Watson no salvaba las aplicaciones sino que les aplicaba la eutanasia. Con un Kernel con alguna excepción, no se puede seguir funcionando. Cualquier intento (teórico) de seguir funcionando podría implicar que se corrompan archivos en el disco, por ejemplo.


Mencionaba hace un rato que en Linux, la pantalla azul se llama Kernel panic. En efecto, eso es. Es un problema en el Kernel y no hay mucho más que hacer, salvo entrar en pánico.


En los SO Windows de hoy, versión 2000 en adelante, los motivos de las pantallas azules son los siguientes. Este gráfico fue generado con datos hasta el mes de Abril del 2004, obtenido del libro Windows Internals de Solomon y Russinovich.



El 15% desconocido se debe a que el nivel de corrupción en la información obtenida después de la caída es tan grande que no permite identificar ningún responsable.


Windows 9X y el festín de las pantallas azules


Por algún motivo de diseño, que jamás sabremos el origen, debido al costo del cambio de transición entre el anillo 3 y el 0, los diseñadores de ese sistema operativo consideraron que para mejorar el rendimiento, mucho código del sistema y aplicaciones correrían en el anillo 0.


Debido a esto, el más mínimo error en cualquier aplicación, podría comprometer el sistema completo, como ocurrió en la archiconocida presentación de Windows 98 de Bill Gates.


Esa fue una lección de duro aprendizaje.


Drivers


Como se pudo ver en el gráfico, el 70% de los problemas son producto de drivers de terceros que corren en el Kernel, y sólo el 5% es código defectuoso de Microsoft. Me gustaría saber cuál es la tasa actual, al final del 2007.


Cuando nosotros como usuarios instalamos un driver que no está certificado, estamos generando un potencial problema con nuestro computador. Por supuesto, ese driver puede estar muy bien desarrollado y jamás tendremos una pantalla azul.


En mi caso particular, el driver de mi webcam tiene problemas serios en Windows XP 64 bits. En Windows XP 32 bits funciona sin problema. En 64 bits, cada vez que abro el Messenger y la activo, pantalla azul. ¿Puede ser Messenger el responsable de la caída de mi equipo? Claro que no. Messenger corre en el anillo 3, pero el driver de mi webcam corre en el 0.


Windows Vista


Según tengo entendido, en Windows Vista, algunos o todos los drivers de terceros se ejecutan en el anillo 2, es decir, con mayor privilegio que las aplicaciones, pero con menor privilegio que el Kernel. Esto ayudaría enormemente a minimizar los problemas de pantallas azules producto de drivers defectuosos. Ya sabemos que para abril del 2004, el 70% de los errores reportados era por problemas de drivers defectuosos.


Parte de ésta información se puede encontrar aquí (sección Driver stability in Windows Vista), pero nada menciona de anillos. Sí menciona que parte del driver corre a nivel de usuario (anillo 3).


Ventaja de Apple


Apple tiene una gran ventaja sobre Microsoft, sin embargo, la posibilidad de instalar Windows sobre un equipo Apple mostrará nuevos aspectos en esta materia.


Como sabemos, el gran problema de Windows son los drivers de terceros.


Windows está hecho para que funcione sobre casi cualquier hardware que sea x86, x64 e IA64 compatible. Como hardware me refiero a placas madre, tarjetas de video de dudosa procedencia, memoria de dudosa procedencia, procesadores de segunda generación de Intel, tarjetas de red y cuanto hardware se pueda construir en el mundo.


Por otra parte, el hardware de los equipos Apple está homologado, probado y garantizado para que funcione bien, como piezas de una buena orquesta.


¿Qué sucede si quiero expandir la memoria de mi Apple? tengo que ir a Apple e instalar la que ellos dicen que funciona.


¿Qué sucede si quiero expandir la memoria de mi PC?. Voy y compro en la esquina la más económica.


Lo mismo para el resto del hardware, y por ende, los drivers.


Drivers malos, problemas de Kernel. Problemas de Kernel, pantallas azules.


Mi amigo Luisón ya instaló Vista en su Mac (también tiene un PC) y dice que anda mucho mejor que en el PC. Yo no puedo atestiguarlo, pero le creo.


Anillo 1


Antes de finalizar, una nota anecdótica. El anillo 1 estuvo mucho tiempo sin utilizarse, o al menos eso es lo que yo tengo entendido. Sin embargo, en estos últimos años se ha usado bastante, aunque probablemente no sepas cómo.


Si utilizaste Virtual Server o Virtual PC, las máquinas virtuales se ejecutan en el anillo 1. Esto significa que tienen más privilegios que tus aplicaciones, pero menos que el Kernel del sistema operativo host. Así, una maquina virtual no podrá hacer caer  tu sistema operativo de host.


¿Anillo -1?


Otra nota curiosa. En hardware que soporta virtualización, quien controla la administración de las máquinas virtuales (Hypervisor en Windows 2008) se ejecuta en el anillo -1, para que las máquinas virtuales se ejecuten en los anillos correspondientes a como ha funcionado siempre (0 y 3). Más información en Ponicke Bloguea.


Espero haber arrojado un poco más de luz sobre el cuénto de nunca acabar de las pantallas azules.


Todos los sistemas operativos mencionados las tienen, y en todos el problema es el mismo.


Saludos, desde Santiago de Chile
Patrick

¿Expresiones regulares?…¿una ayuda por favor?

Si hoy tuviese que dar una lista de las funcionalidades más poderosas de cualquier lenguaje y que son menos usadas, sin duda incluiría en esta lista las expresiones regulares.


Hay que ser honestos y reconocer que existen un par de barreras de entrada importantes para su uso.




  1. Documentación. No existen muchos sitios dedicados a explicarlas. Hay unos muy buenos, pero son pocos.



  2. Dificultad de aprendizaje. No son triviales.


Si tienes conocimientos básicos de estas y siempre has tenido problemas probando y probando, te recomiendo este sitio. Tiene un probador que usa colores y te muestra si tu información cumple con la expresión definida, y si no lo hace, hasta dónde llega.


La URL es regexpal.com y ésta es una imagen de su utilización. Muy útil.


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

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


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


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




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



  • La existencia de Option Strict en vb.net.



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


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


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


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



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


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


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


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


Vamos a la prueba y a los sorprendentes resultados.


Código a ejecutar


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


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


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



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



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


Resultados de las pruebas


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



















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

 


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


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


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


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


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


Nuevas pruebas, sin Replace


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



















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

 


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


Conclusiones aventuradas


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


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



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



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


Consumo de recursos


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


Los contadores a medir son:




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



  • Excepciones lanzadas por segundo



  • Colecciones en cada generación



  • Colecciones inducidas



  • Total de bytes utilizados



  • Bytes pedidos por segundo



  • Tiempo utilizado por el GC



  • Bytes privados del proceso


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


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


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


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


Versión con Replace




















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


Versión sin Replace




















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


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


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


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


Versión con Replace




















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


Versión sin Replace




















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


¿¿¿¿Qué????


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


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



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


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


Consumo de recursos, V 2.0


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


Versión con Replace



















































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

 


Mini conclusiones versión con Replace


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


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


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


Versión sin Replace



















































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

 


Mini conclusiones versión sin Replace


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


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


Conclusiones


Las siguientes conclusiones puedo obtener del análisis.


1.- No correr aplicaciones 1.1 en 64 bits


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


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


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


Saludos,
Patrick.
 

¿Cómo reducir el costo de un LookUp de SQL Server?

Durante la exposición de nuestra charla de como optimizar consultas de SQL Server, hice bastante hincapié en la relación/dependencia de los índices no agrupados (no clustered) y el agrupado (clustered) de un tabla. En caso de que ésta no cuente con un índice agrupado, existe una relación/dependencia similar pero con una estructura interna de la tabla (RID).


En este post veremos qué es un LookUp (SQL 2005) o Bookmark LookUp (SQL 2000) y cómo eliminarlos o al menos mitigarlos cuando se pueda. Usaré SQL 20005, pero si tienes 2000, la manera de enfrentarlo es similar.


Antes de ir a la información del caso, haremos un pequeño repaso de lo revisado en la charlas de optimización de consultas.


Relación/dependencia entre índices en una tabla


Un índice no agrupado almacena, por cada elemento del índice, un “identificador” que dice donde encontrar en la tabla los datos asociados a este ítem del índice. De esta forma, cada vez que SQL Server utiliza el índice no agrupado y determina que debe ir a buscar los datos, utiliza ese “identificador” para ubicar los datos en la tabla.


Si la tabla no tiene un índice agrupado, el identificador utilizado es el identificador de la fila o row id (en inglés), el cual se representa con las siglas RID. Este RID está compuesto por el numero del archivo de la base de datos, el número de la página dentro de ese archivo y por último, el número de la fila en la página.


Si la tabla tiene un índice agrupado, el identificador utilizado es la llave del índice agrupado. La llave del índice agrupado son TODAS las columnas del índice agrupado. Por favor, no confundir la llave del índice con la llave primaria de una tabla, aunque muchas veces tienen el mismo “valor,” en especial cuando la tabla se creó sin darle mayor importancia a este tema.


Para más información, revisa el BOL (Books On Line) de SQL Server, en la instrucción Create Index.


Bookmark LookUp o LookUp a secas


En SQL Server 2000 se le conoce como Bookmark LookUp. En la versión 2005 se llama solamente LookUp. La idea es la misma para ambas versiones; El problema es el mismo, las soluciones similares.


LookUp, traducido al español, significa operación de búsqueda, y representa exactamente eso. Realiza una operación de búsqueda, con un “identificador” de una fila, para traer más información, de la fila específica, desde la tabla.


La búsqueda se hace, como ustedes se imaginan, utilizando el RID si la tabla no tiene un índice agrupado, o utilizando la llave del índice agrupado, si la tabla cuenta con uno de ese tipo. La siguiente imagen sirve para aclarar el concepto que se trata de explicar.







Utilizando el índice no agrupado, primero se identifica las filas que cumplen con la condición de filtrado.


Resultado: Celdas amarillas.


 


 


Se usan los tres identificadores de filas (rojo, verde y azul) y se realiza la operación de búsqueda en el índice agrupado.


Si la tabla no tiene índice agrupado, la operación es similar, pero usa el RID.


Se retornan los datos faltantes.


Veamos entonces ahora información del caso. En esta oportunidad reproduciré el escenario en una base de datos de prueba, con unas tablas creadas para la ocasión.


LookUp muy costoso


Nota: Hace un tiempo atrás estaba en un curso de optimización de SQL Server y le hice la siguiente pregunta a la instructora: “Si tengo el plan de ejecución de una consulta, y el operador de lookup es responsable de casi el 100% del costo del plan, ¿cómo puedo mejorar el rendimiento de mi consulta?.”


Tristemente, no hubo una respuesta aclaratoria ante mi pregunta, como ocurrió en varias otras además. Hoy en día, retrospectivamente, pienso que la instructora era lo que nosotros acá en Chile llamamos “un Adán”, de Adán y Eva. ¿Por qué?. Porque si a Adán le sacas la hojita, queda desnudo. En este caso, la “hojita” es el “libro del curso.”


Si has estado tratando de optimizar consultas de SQL, es probable que ya te hayas topado con este problema. ¿Qué hacer cuando la operación LookUp es muy costosa?


Antes de comenzar, estructura de tablas


Para este ejemplo, utilizaremos dos tablas, t1 y t2, con la siguiente estructura cada una






CREATE TABLE [dbo].[t1](
    [c1] [int] IDENTITY(1,1) NOT NULL,
    [c2] [varchar](50) NOT NULL,
    CONSTRAINT [PK_t1] PRIMARY KEY CLUSTERED ([c1] ASC) ON [PRIMARY])
ON [PRIMARY]

CREATE TABLE [dbo].[t2](
    [c1] [int] IDENTITY(1,1) NOT NULL,
    [c2] [int] NOT NULL,
    [c3] [varchar](50) NOT NULL,
    [c4] [tinyint] NOT NULL CONSTRAINT [DF_t5_c4] DEFAULT ((1)),
    [c5] [char](200) NOT NULL CONSTRAINT [DF_t2_c5] DEFAULT (”),
    CONSTRAINT [PK_t2_1] PRIMARY KEY CLUSTERED ([c1] ASC) ON [PRIMARY])
ON [PRIMARY]

CREATE NONCLUSTERED INDEX [idx_c2] ON [dbo].[t2] ([c2] ASC)
ON [PRIMARY]

ALTER TABLE [dbo].[t2] WITH CHECK
ADD CONSTRAINT [FK_t2_t1] FOREIGN KEY([c2]) REFERENCES [dbo].[t1] ([c1])


La tabla t1 tiene 100.000 registros. La tabla t2 tiene 1.000.000 registros. La consulta que estamos realizando es la siguiente.






select t1.c1, t1.c2, t2.c1, t2.c3, t2.c4
from t2 inner join t1 on t2.c2 = t1.c1
where t2.c2 between 1000 and 2000 and t2.c3 like ‘2%’ and t2.c4 = 1 


Con esta consulta, estamos trayendo un cantidad importante de columnas de la tabla t2, como también las dos columnas de la tabla t1. Existe un índice en t2.c2, por lo tanto, el filtro between 1000 and 2000 se realizará utilizando el índice para reducir las filas.


El siguiente es el plan de ejecución generado, en versión texto y gráfico.






Table ‘t2’. Scan count 1, logical reads 30682, physical reads 0, …
Table ‘t1’. Scan count 1, logical reads 6, physical reads 0, …

Rows Executes StmtText
——————————————————————————————-
561 1 |–Merge Join(Inner Join, MERGE:([test].[dbo].[t1].[c1])=([test].[dbo].[t
1001 1 |–Clustered Index Seek(OBJECT:([test].[dbo].[t1].[PK_t1]), SEEK:([
561 1 |–Filter(WHERE:([test].[dbo].[t2].[c3] like ‘2%’ AND [test].[dbo].
10010 1 |–Nested Loops(Inner Join, OUTER REFERENCES:([test].[dbo].[t2
10010 1 |–Index Seek(OBJECT:([test].[dbo].[t2].[idx_c2]), SEEK:(
10010 10010 |–Clustered Index Seek(OBJE…]) LOOKUP ORDERED FORWARD)


Si miramos la versión gráfica, podremos observar que el 100% del costo del plan esta en la operación lookup. Notar que hemos marcado algunos textos en blanco arriba, los cuales revisaremos más adelante. Para ver el plan gráfico más grande, haz clic sobre él.







En el plan gráfico, podemos ver que la operación Key Lookup es responsable del 100% del costo de plan, con un costo de 22,06 (muy malo para un operador, y en consecuencia para el plan). Complementando con el plan de texto, en éste aparece la palabra LOOKUP, y está usando el índice agrupado (clustered) para recuperar las demás columnas que necesita.


Además, se realizan más de 30.000 lecturas sobre la tabla t2. 


¿Donde está el problema?¿Por qué tiene un costo tan elevado?


Una única operación de LookUp es de bajísimo costo, más aún si utiliza un índice para hacer un seek a una fila específica en la tabla. Sin embargo, si se ejecuta 10.010 veces, la sumatoria de estos costos pequeñísimos pasa a tener relevancia, como es nuestro caso.


La cantidad de veces que se ejecuta la operación LookUp se puede ver en 2 partes. Una de ellas es el texto marcado con blanco en el plan de ejecución de texto y la otra es en el Tooltip amarillo, en “Actual Number of Rows.”


Solución[es] al problema


Existen dos formas de enfrentar este problema, y por supuesto, dependen de cada escenario.


Una de ellas, y que no veremos ahora porque no tienen ninguna gracia, es agregar todas las columnas necesarias al índice, y con eso, evitar la operación LookUp. Si tengo toda la información en el índice, no necesito ir a buscar más columnas a la tabla. Esta no falla nunca, pero uno debe evitar incluir muchas columnas en los índices.


La segunda solución, y que es mucho más elegante, sólo es posible realizar en un escenario como el que tenemos ahora.







Si miran con detenimiento el plan de ejecución, el operador filter está filtrando en las columna c3 y c4, aplicando los criterios definidos en la consulta SQL. Esto es esperable.


Más aún, como resultado del filtrado, la cantidad de filas retornadas disminuye (anchos de las flechas de entrada y de salida del filter). Esto también es esperable, ¿o no?

 
 

Si todo es esperable, ¿dónde está la gracia?


El costo del operador LookUp es muy alto porque se ejecuta muchas veces, ¿verdad?. ¿Qué sucedería si logro reducir la cantidad de veces que deba ejecutarse? Seguramente el costo de la consulta será menor y el rendimiento de ésta mucho mayor.


Para reducir la cantidad de veces que se ejecuta, debo reducir la cantidad de filas que son retornadas luego de que se filtre utilizando el índice idx_c2. Para reducir la cantidad de filas retornadas, agrego una columna más al índice, la que más me ayude a filtrar.


Debido a que la selectividad de la columna c3 es mejor que la de c4, incluiré sólo la columna c3 en el índice idx_c2. Con eso, reduciré la cantidad de filas que requerirán un lookup de más información. Recuerden que estamos tratando de minimizar el costo del LookUp. Si se quiere suprimir totalmente, se deberán agregar todas las columnas necesarias al índice, solución que deberá dejarse sólo para casos ultra-extremos.


Veamos el plan de ejecución de texto de la misma consulta, pero con el índice idx_c2 incluyendo a la columna c3 además de la c2 que ya tenía.





Table ‘t2’. Scan count 1, logical reads 3332, physical reads 0, …
Table ‘t1’. Scan count 1, logical reads 6, physical reads 0, …

Rows Executes StmtText
——————————————————————————————-
561 1 |–Merge Join(Inner Join, MERGE:([test].[dbo].[t1].[c1])=([test].[dbo].[t
1001 1 |–Clustered Index Seek(OBJECT:([test].[dbo].[t1].[PK_t1]), SEEK:([
561 1 |–Filter(WHERE:([test].[dbo].[t2].[c4] = (1)))
1075 1 |–Nested Loops(Inner Join, OUTER REFERENCES:([test].[dbo].[t2
1075 1 |–Index Seek(OBJECT:([test].[dbo].[t2].[idx_c2]), SEEK:(
1075 1075 |–Clustered Index Seek(OBJE…]) LOOKUP ORDERED FORWARD)


 







Ahora sólo ejecutó 1.075 veces el LookUp. No nos hemos deshecho de él, pero al menos hemos reducido su impacto en el costo de la consulta.  


Las lecturas lógicas fueron reducidas desde más de 30.000 a sólo 3.332. Una excelente optimización.


El nuevo costo del operador LookUp es 3,58, como se puede observar a la derecha en el Tooltip. El costo antes de modificar el índice era de 22,06. Otra excelente reducción en el costo.

 

Conclusión


Hemos visto como minimizar el impacto de la operación LookUp en SQL Server. No vimos como eliminarlo ya que para lograr eso, el camino es directo y simple. A veces no es necesario eliminarlo para mejorar drásticamente el rendimiento de una consulta.


 


Hasta la próxima, desde Santiago de Chile
Patrick.
 

Agatha Veloce

Bastante tiempo ha pasado desde mi última publicación. Inicialmente mi meta era escribir dos o tres artículos técnicos al mes, que sean de utilidad al menos para mi como ejercicio demostrativo, y esperando aportar a la comunidad hispano parlante.


Sin embargo, estos últimos dos meses han sido duros. No soy de escribir de mi vida privada, porque creo que la vida privada le interesa solo a uno mismo, pero en este caso lo haré sólo por la terapia de hacerlo, en especial porque quiero expresar a través de éste post, agradecimiento y decir algunas palabras de quien sería una de mis compañeras más fieles por más de 7 años.


Agatha


Con casi 8 años de vida, que para un perro puede ser bastante, o al menos eso me enseñaron cuando chico, una de mis chicas ya no está más con nosotros. Se llamaba Agatha, aunque le habíamos puesto muchos sobrenombres como seguro tu lo haces con tus mascotas. Sus sobrenombres fueron:




  • Agatha Veloce (Veloce es veloz en italiano) o simplemente “Veloce” o “Velocita”: se lo pusimos en un campo, cuando velozmente perseguía unas gallinas para comérselas. No la dejamos, pero nos costó bastante agarrarla.



  • Agatha Tanque, Agatanque y Tanqueta: Cuando la sacábamos a pasear con correa, hacía un movimiento agachando el cuerpo y pegando como unos tirones estirando las piernas traseras. Mucha fuerza, como un tanque.



  • Agatiuska, Agatiti: No recuerdo el origen de estos sobrenombres.



  • Agatha salvaje: Nació en el mismo episodio de las gallinas.



  • Sra de Piltrow: Nunca llegamos a ponerle este sobrenombre, pero como fue “pareja” del perro de Claudio (no es que mi amigo sea un perro [:)]), y como el perro de él se llama “Sir William Piltrow Tercero”, Agatha era la “señora de Piltrow”. Nunca logramos que se cruzara, o Sir William era muy “Sir” para sus cosas.


 

Llegó de dos meses a casa, junto a sus hermanas Tuto y Daisy. La Tuto iba de regalo a mi madre, la Daisy era de mi ex y la Agatha era mía. Tiempo después, Daisy se escapó y murió atropellada. La tuto aun vive, mejor que nunca, en el Sur de Chile junto a mis padres. La tuto ya fue madre y abuela.


La Agatha casi nos deja antes, pero en su atropello tuvo mucha suerte. La única lesión que tuvo fue la perdida de la sensibilidad en el lomo. Nos dimos cuenta cuando sentíamos olor a quemado y era la Agatiti que se había puesto tan pegada a la estufa que se le quemaban los pelos, y andaba con mechones amarillos. Desde ese momento, tuvimos que tener mucho cuidado con la estufa.



Después de la partida de la Daisy, llegó la Mikaela o “Mikin Kakurri” (foto). El apodo Kakurri nació porque tiene una fascinación en comerse los excrementos de los gatos (en Chile, usamos la palabra Caca para los excrementos, y de Caca a Kakurri hay un solo paso).



Hace ya casi un año nació mi primer hijo, Baltazar y en conjunto con mi actual señora decidimos, aceptando la invitación de mis padres, mandar las chicas (Agatha y Mika) a vivir al Sur de Chile, en compañía de los “abuelos”, de su hermana Tuto, y su sobrina Susana y su hija Olivia.


Con mucho dolor acepté enviarlas al sur. Sabía que con la llegada de Baltazar iba a ser difícil y después de un mes de vivir en la casa, decidí que el que las chicas pasaran todo el día en el patio y que yo las dejase entrar 5 minutos en la noche, no era justo para ellas, y más aún cuando habían vivido toda su vida dentro de la casa, compartiendo casi todo.


Al menos estoy convencido que el último año de la Veloce en el sur fue súper. Nada como perseguir conejos y cuanto animal hay en el sur. Entre eso y los 40 metros cuadrados del patio donde pasó a ser recluida después de ser “expulsada de la casa”, creo que no hay donde perderse. Además, compartió con la Mika, Tuto, Susana y la Olivia, además de los perros de las otras parcelas, y con el constante regaloneo de los “abuelos.”


Hace dos meses más o menos le encontraron una cantidad de tumores tan grande que el veterinario prefirió no operarla. Si la operaba, o se moría ahí mismo o muy poco tiempo después, con un sufrimiento innecesario. Conversamos con mis padres y preferimos no operarla. Supongo que ella habría querido lo mismo….nunca lo sabremos.


Hoy ya no está con nosotros, pero está en los recuerdos que tenemos. La Mika ya está mejor, porque las últimas semanas tampoco fueron fáciles para ella. Me hubiese gustado que Baltazar hubiese podido jugar con ella porque era una perra que jamás había mordido a nadie, buenísima con las personas y muy muy dócil. El perro ideal para que un niño conozca.



Hasta siempre Velocita.


Bueno, no todo es triste.


Otros eventos


Poniéndonos aburridos, paso a detallar los eventos de las últimas semanas:




  • Cambio de casa. Quien se haya cambiado, sabe lo que esto significa. Quien no, prepárese.



  • Tener que luchar contra la negligencia del Banco de Chile y soportar la huelga de 2 semanas del Banco Itaú  para que nos aprobasen el crédito de la casa. En resumen, un trámite de 4 semanas terminó de ejecutarse en 7 semanas. Increíble.



  • Hemos dado hasta hoy, tres veces la misma charla de optimización de consultas. Dos veces como eventos de MSDN Chile y otra en un partner de Microsoft junto a todo el departamento de desarrollo. Esta empresa decidió detener a todo su departamento (30 personas aprox.) por cuatro horas y media para presenciarla. Muy reconfortante.



  • Operaron de apendicitis de mi señora. Todo bien y tranquilo.



  • Dentición de Baltazar, además de resfrío y otitis.


Estos últimos dos eventos llegaron juntos. Mientras mi señora estaba en la clínica, me tuve que hacer cargo de Baltazar dos días seguidos. Me saco el sombrero por las personas que se encargan de criar a los hijos. Quien diga que no es trabajo, no sabe de lo que habla. Aunque hay que reconocer que si tuviésemos un par de pechugas, sería un poco más fácil.


¿Y ahora?


Y bueno…ahora estoy trabajando en un post sobre optimización de consultas en SQL Server, pero orientado sólo a resolver problemas de Lookup (SQL 2005) o Bookmark Lookup (SQL 2000).


Saludos,
Patrick

Charla de optimización de consultas de SQL

Aunque no ha faltado trabajo, las últimas semanas las pasé preparando material para una charla de optimización de consultas en SQL Server que dimos hace algunos días.


Ya ha pasado casi una semana desde que dimos la charla, a la cual asistieron, si mal no recuerdo, 139 personas. Todo salió casi perfecto. Nos faltó tiempo para hacer las demos, aunque los asistentes nos aguantaron estoicamente desde las 6:35 pm hasta las 10:15 pm, 3 horas y 40 minutos. Impresionante.


Algo muy gratificador, y que no siempre ocurre lamentablemente, es poder lograr buena interacción con los asistentes. Usualmente no preguntan mucho, pero en esta oportunidad, estimo que se deben haber realizado 30 o más preguntas y contra preguntas. Así da gusto!!.


La imagen que usamos como ícono de la charla causó risas y simpatía entre nuestros amigos más cercanos.



Los principales temas abordados fueron:




  • Definición de plan de ejecución, como se construye, que elementos influyen en éste,



  • Detección de problemas en planes de ejecución



  • Evaluación de costos, lecturas lógicas



  • Teoría de índices, relaciones entre índice agrupados y no agrupados



  • Buenas prácticas, selectividad, estadísticas, fill factor, entre otras cosas.


Aquí van algunas fotos de la preparación de la charla, el evento propiamente tal y la celebración al final. No incluí fotos con asistentes porque uno nunca sabe si quieren o no aparecer. Para ver las fotos más grades, pincha en éstas.












 

 


Si te interesa ver el contenido de la charla, ésta está publicada en [cafeina], precisamente éste link.


Estamos viendo de realizar una continuación de la charla, sólo para los registrados de ésta, con el fin de poder terminar las demos y resolver más dudas.


desde Puyehue, al sur de Chile


Patrick


PD: Esta es la vista desde mi habitación acá en el sur de Chile [:)]


 

Entre las excepciones y la flojera de los desarrolladores

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


¿A qué me refiero?


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


Vamos al caso


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


Pruebas de carga


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


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



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


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



Asombrosa similitud, ¿no?


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


¿Resultado?


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






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


Esta es parte del stack manejado, del mismo thread.





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

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






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


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







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


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


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


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


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


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


Nueva prueba de carga


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



Conclusión


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


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


Adicionales


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


Otros sitios web interesantes:



Saludos,
Patrick.

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

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


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



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


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


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


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


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


Excepción lanzada


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


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






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


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


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


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


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


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


Código del cliente


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


Reflector al rescate


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






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


¿Y esa línea?… Interesante.


Necesitamos más pistas


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






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


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



  1. Error en el código de Add

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

  3. Algo más

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


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


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


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


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






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


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


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


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


Recopilando pistas


Tenemos las siguientes pistas:



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

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

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

  4. Atributo de nombre sospechoso (debug)

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


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


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


Código de máquina


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


desde Santiago, Chile
Patrick.

¿Microsoft y Van Halen?

Siendo un fanático de Van Halen, bueno, no tanto, pero me gusta mucho la música de ellos, en especial de la era Hagar, he estado revisando últimamente el sitio web para saber que ha pasado con la banda.


Además de los tradicionales problemas de drogas y de relación entre seres humanos, la última visita a www.vanhalen.net entregó este peculiar mensaje.


If you have registered for our newsletters in the past, please re-register your information, as Microsoft has deleted our database.


Para los escépticos, pueden visitar el sitio y verlo por sus propios ojos, o ver la siguiente imagen capturada desde este.



Que yo sepa, Microsoft no anda borrando bases de datos, ni menos de un grupo de música. A lo mejor no sé toda la historia.


desde Santiago
Patrick.