martes, 27 de octubre de 2009

Tuneando la performance de sitios web

Este post describe distintas técnicas de optimización de performance para sitios web. Se basa en la experiencia de una aplicación de reporting construida en asp.net con fuertes constraints de performance (tiempo de ejecución por debajo de 2 segundos para accesos locales y 8 segundos para accesos remotos), teniendo una conectividad pobre desde sitios remotos.
Sin embargo estas técnicas aplican a cualquier aplicación tipo web/html. Las implementaciones y ejemplos de código se corresponden a una aplicación asp .net 2.0, c#, IIS 6, pero pueden extenderse a otras tecnologías.
Un buen lugar para empezar a abordar este tema es el sitio de best practices de yahoo (se pueden encontrar muy buenas recomendaciones):

Según una investigación realizada por ellos (http://yuiblog.com/blog/2006/11/28/performance-research-part-1/), al acceder a una aplicación web el tiempo de download se distribuye un 20% en html y el 80% restante en otro tipo de contenido (javascript, imágenes, etc.) Por lo tanto la primera recomendación es:

Minimizar la cantidad de gets (roundtrips)

Esto puede conseguirse de distintas maneras:

Unificar archivos javascript

En nuestro caso tenemos un archivo .js para cada funcionalidad, de manera tal de hacer el desarrollo más modular. Al armar la version release los unificamos y comprimimos en un único archivo. Lo mismo se puede hacer para las hojas de estilo. Se debe tener en cuenta que cuanto mejor formado esté el código javascript (usando llaves para todas las instrucciones aunque no sea necesario, ";" al final de cada línea, etc.) mayor será el nivel de compresión que se puede aplicar.

Para unificar los archivos utilizamos una simple pagina .aspx que contiene un include a cada fuente javascript. Luego la aplicación detecta si está en modo debug o relase (usando HttpContext.Current.IsDebuggingEnabled) y en función de esto genera del lado del server las referencias a fuentes javascript necesarias.

Juntar varias imágenes en una (css sprite)

Esta ténica se conoce como css sprite. Básicamente se arma una imagen con las imágenes más utilizadas por la aplicación y se referencia con estilos a porciones de esta imagen. Ejemplo:
Definicion de clase para icono de ayuda:
.imgHelpIcon { background: url('/App/Cached/sprite.gif') 0 0 no-repeat; font:1pt; background-position: 10 -20px; width: 15px; height: 15px; }

Uso del estilo:
<span class="imgHelpIcon"></span>

Utilizar cache infinito para recursos estáticos

Esta técnica tiene gran impacto en el download, sobre todo si la mayoría de los usuarios utilizan frecuentemente la aplicación. Con esto se evitan los gets a estos recursos, incluso si el cache del browser está configurado como "every visit to the page".

En nuestro caso lo implementamos de la siguiente manera. Configuramos en el webserver una carpeta cached con fecha de exipiracion en 2035. De esta manera disponemos de 2 altenativas para almacenar los recursos estáticos (con y sin cache). Por ejemplo en modo debug la aplicación utiliza los scripts de la carpeta app/scripts. En cambio, en modo release utiliza app/cached/scripts.

Para los recursos que pueden cambiar (como js y css) los versionamos agregando un parametro a la url. Por ejemplo:
<script scr="cached/all_js.js?v=1.01"></script>

Para recuperar este valor usamos el método ConfigurationManager.AppSettings.Get("jsVersion"). Cada vez que se arma un release, incrementamos este número solo si se realiza un cambio sobre algún fuente javascript.

Aplicando esta técnica a los recursos estáticos, los gets de usuarios frecuentes se redujeron a práctiamente solo uno para obtener el html (ya que el browser recupera todos los recursos estáticos del cache). Esta técnica mejora notablemente los tiempos de acceso desde clientes remotos.
Quitar seguridad integrada a recursos estáticos

Los recursos con seguridad integrada implican 2 o 3 roundtrips al server según el tipo de autenticación. Por lo tanto es conveniente evitarla siempre
que sea posible.

Quitar seguridad integrada a servicios ajax

En algunos servicios ajax se necesita información del usuario para resolver ciertas funcionalidades. Para evitar la seguridad integrada, el usuario forma parte del estado en el server (más adelante se explica mejor cómo resolver esta cuestión). De esta manera todos los servicios ajax se ejecutan sin seguridad integrada.

Quitar seguridad integrada de aplicación.

Aplicar esta técnica nos llevó un poco más de trabajo. Básicamente solo una página (authenticate.aspx) está configurada con seguridad integrada. Cuando el usuario ingresa a la aplicación, esta intenta obtener el usuario del estado. Sino lo puede obtener, entonces asume que no está autenticado, lo redirecciona a authenticate.aspx para que obtenga las credenciales del usuario, lo guarde en el estado y lo redireccione nuevamente a la aplicación. Luego el usuario navega por la aplicación sin seguridad integrada (la aplicación verifica que el usuario esté autenticado usando una variable de sesión).

Al implementar este mecanismo nos encontramos con un problema grave. IE 6 tiene un bug donde a veces no envía los parámetros enviados por post (http://blogs.msdn.com/david.wang/archive/2005/12/01/HTTP-POST-Fails-for-Anonymous-Authentication.aspx). Esto lo solucionamos configurando acceso anónimo y seguridad integrada a la vez.

Si reducimos todo lo posible la cantidad de pedidos al server (GETs) lo siguiente que podemos hacer es reducir el tamaño de la información intercambiada.
Reducir el tamaño de los gets / responses

Comprimir javascript y css

Existen varias herramientas para hacer esto (http://developer.yahoo.com/yui/compressor/). Estas herramientas básicamente eliminan espacios, comentarios, incluso algunas hasta ofuscan el código para reducir su tamaño.

Reducir tamaño html

La idea es simple, cuanto menos se envíe, más rápido va a terminar la transferencia. Se pueden hacer varias cosas para reducir el html:
  • Eliminar espacios
  • Utilizar nombres abreviados para los nombres de estilos y objetos
  • Maximizar el uso de hojas de estilo, dejando el html mucho más limpio. Una herramienta muy útil para hacer esto son los css selector (http://www.w3schools.com/Css/css_syntax.asp)
  • Manejo de estado
La aplicación persiste el estado en el server. En lugar de utilizar campos hidden en el cliente (que se envían en cada postback), utiliza variables de sessión para persistir los cambios realizados sobre un reporte (filtro, paginado, drill, etc.). De esta manera el cliente solo envía al server lo que cambia, reduciendo considerablemente el tamaño de los requests.

Mantener estado en el cliente usando user behaviour

Esto se puede aplicar en los casos que solo se necesita persistir información en el cliente y esta no es requerida por el server (ejemplo: estado de apertura de un árbol). Internet explorer ofrece un behavour que permite persistir información en el cliente (incluso cuando el usuario cierra el browser):

Ajax + json / xml

El los casos donde los servicios ajax devuelven datos, usamos json ya que es más liviano que xml. Solo en algunas ocaciones, cuando el servicio ajax devuelve html, usamos xml. En este caso el html se devuelve dentro de un cdata para evitar problemas de encodeo.


Otrás técnicas que no usamos, pero también son de ayuda

Compresión gzip

Es posible configurar IIS para que comprima gzip archivos javascript y estilos. El beneficio de la compresión gzip es notorio cuando se accede remotamente. También es posible comprimir recursos dinámicos como páginas aspx.

Usar varios dominios para el download de contenido

Por default el browser usa 2 conexiones por dominio para hacer el download. Si el contenido cambia (como ocurre por ejemplo en los portales), es posible utilizar distintos dominios para el mismo, de manera tal de aumentar la cantidad de conexiones usadas por el browser.

En el sitio de yahoo hay otra recomendaciones:

Herramientas

En nuestro caso fue fudamental usar máquinas virtuales como entorno de desarrollo para poder experimentar con estas técnicas y contar con máquinas tipo desktop estandar en sitios remotos para poder evaluar el impacto de las mismas. También nos sirvió de mucho usar un debugger http como fiddler (http://www.fiddlertool.com/fiddler)

Fiddler permite ver gráficamente cómo es el tiempo de download. Para ello, se debe activar el modo streaming (http://www.fiddlertool.com/Fiddler/help/Streaming.asp). Con esta herramienta es fácil encontrar puntos de mejora, revisar errores http 404, ver el impacto de aplicar estas técnicas, etc.

jueves, 22 de octubre de 2009

Cuál es la instalación de paneles solares más sustentable?

Desde hace un tiempo vengo trabajando en esta idea. Conseguí una lista de los proveedores en Argentina (http://www.energiaslimpias.org/categoria/informes-energia-solar/) y me contacté con todos para consultar por una instalación para hacer andar una notebook (80w) durante 8hs x día.

La típica instalación está formada por los paneles solares, regulador (para no sobre cargar la batería), batería (para acumular la energía y usarla cuando no hay sol), inversor (para convertir los 12v de continua de la bateria a 220v de alterna). Encuentro algunas desventajas a este tipos de instalación:
  • Son costosas => difícil que otros quieran implementarlas también.
  • Podrían ser mas eficientes, ya que se pierde energía en el regulador, inversor y cargador de notebook.
  • Requiere varios paneles solares (los paneles se colocan orientados al norte, a 50 grados aproximadamente).
  • Plantean el problema de qué hacer con la batería cuando finaliza la vida útil de esta.
Por lo que se me ocurren algunas variaciones sobre este esquema básico:
  • Usar un único panel solar y orientarlo al sol según la hora. Tendría que buscar la manera de automatizar esto... O podría combinarlo con la técnica pomodoro y aprovechar los breaks para subir a la terraza (me levanto de la silla) y acomodarlo a mano.
  • No usar un inversor y trabajar siempre en corriente continua. Necesitaría un cargador para la notebook que a partir de 12v genere los 19.5 que necesita (encontré uno).
  • Si en lugar de usar batería para acumular la energía, utilizara directamente la batería de la notebook? Como podría hacer para no afectar la vida útil de esta? Luego tendría que ver que hacer con la energía que no uso. Podría volcarla a la red, como hacen en otros paises, o regalarla a mis vecinos.
  • Sirve el regulador para que los paneles generen 12 v constante?
Algo interesante de no usar baterías, es que además de disminuir el costo y contaminar menos, abre nuevas posibilidades que requieren pensar de manera colaborativa respecto a la comunidad en donde vivimos.

saludos!

domingo, 18 de octubre de 2009

Parser para un simple DSL

Dado que estoy empezando a usar un DSL, me gustaría compartir mi experiencia, por si otros deciden incursionar en el mundo de los DSL. Voy a aprovechar para contar que me lleva a usar un DSL.

Uno de los problemas de mi dominio es resolver el binding de los filtros, pertenecientes a mi domain model, en una consulta MDX. Para eso adopté la siguiente sintaxis:

SELECT {[Measures].[SaleAmount], [Measures].[Profit]} ON COLUMNS,
NON EMPTY {[Brand].[All].Children} on rows
FROM [Sales]
WHERE (${category}, ${product}, ${year})

donde si category=A, product=B, y year no tiene valor, la query debería valer:

SELECT {[Measures].[SaleAmount], [Measures].[Profit]} ON COLUMNS,
NON EMPTY {[Brand].[All].Children} on rows
FROM [Sales]
WHERE (A, B)

MDX es un DSL usado para resolver consultas multidimensionales. Lo que estoy haciendo es embebiendo mi DSL adentro de otro DSL (por ahora es tan simple, que no merece llamarse DSL).

El primer paso para usar mi DSL es parsear los ${...} para extraer la expresión del mismo. Seguramente hay muchas formas de parsear los ${...}. Yo decidí usar un tokenizer basado en palabras, que devuelve el texto hasta encontrar el token buscado. Esta es la interface de WordTokenizer:

la implementación basada en la clase String:

y las pruebas unitarias (aguante TDD):

Es simple extraer es las expresiones de mi DSL usando el word tokenizer:

Con esto extraigo mi DSL de la consulta MDX. Este por ahora es simple, pero hay casos que tienen un poco más de complejidad.

Por ejemplo, si un filtro no tiene valor, tengo que aplicar el valor default:

SELECT {[Measures].[SaleAmount], [Measures].[Profit]} ON COLUMNS,
NON EMPTY {[Brand].[All].Children} on rows
FROM [Sales]
WHERE ${year.default([Time].[Year].[2009])}

O distintos filtros que forman una jerarquia (mes, trimestre, año) y solo debe reemplazarse un valor en la consulta:

SELECT {[Measures].[SaleAmount], [Measures].[Profit]} ON COLUMNS,
NON EMPTY {[Brand].[All].Children} on rows
FROM [Sales]
WHERE ${month.parent(quater).parent(year)}

O una combinación de jerarquías y valores default. Para resolver estos y otros casos, usé lo que se llama fluent interface, que permite fácilmente armar DSL para un problema de dominio (es fácil de leer y simple de programar).

Para parsearlo uso un simple StringTokenizer:

La implementación es bastante simple. Seguramente hay casos que no se cubren, pero sobre esta base es fácil agregar nuevos métodos al DSL.

Hay otros conceptos interesantes asociados a un DSL, como:

saludos!

martes, 6 de octubre de 2009

Problema paneles solares y mapa mental

Gracias a http://www.softwarebyrob.com/2009/10/06/micropreneur-spotlight-online-mind-mapping-software-thoughtmuse/ encontré una herramienta en la web para armar mapas mentales. Me pareció muy buena para organizar las distintas aristas de un problema. Asi que aproveché para hacer el ejercicio de alimentar una notebook con paneles solares

Aportes son bienvenidos!

domingo, 4 de octubre de 2009

Expandir disco guest ubuntu, host vista (vmware player)

Hoy me pasó que la virtual con ubuntu se quedó sin espacio en disco (la imagen original tiene 8 GB). Mi sistema operativo host es windows vista y la tengo una virtual machine con Ubuntu Desktop 9.04 (con vmware player).

Estos son los pasos para agrandar el tamaño del disco a 100 GB.

1. Convertir el disco a 2GB split not pre-allocated usando vmware converter. Se puede bajar de la página de vmware.

2. Crear con easyvmx una imagen ubuntu de 100GB. De paso aproveché para configurar 2 procesadores:
http://www.easyvmx.com/cgi-bin/create-easyvmx.cgi?filetime=2009-10-04-23:08:06&vmname=Ubuntu+9.04+Desktop&guestOS=ubuntu&memory=2048&numvcpus=2&vmdesc=UbuntuDesktop&vmlname=UbuntuDesktop&vmurl=http://www.kudewe.com/&hwaddr0=00:0c:29:46:9c:10&conntype0=nat&devtype0=vlance&hwaddr1=00:0c:29:51:cf:6c&conntype1=nat&devtype1=vlance&floppyfile=autodetect&isodev=autodetect&ide1_1file=.iso&disk0_0file=100Gb&disk0_1file=4800Mb&sound=yes&soundcard=es1371&usb=yes&usbconnect=FALSE&com1=yes&com1_flow=TRUE&com2_flow=TRUE&lpt1=yes&lpt1_bidir=TRUE

3. Copiar de la maquina virtual original los archivos de disco Ubuntu_9_04_Desktop-s001.vmdk, Ubuntu_9_04_Desktop-s002.vmdk, Ubuntu_9_04_Desktop-s003.vmdk, Ubuntu_9_04_Desktop-s004.vmdk. No copiar el Ubuntu_9_04_Desktop-s005.vmdk (último archivo)

4. Bajar gparted.iso. Configurar la vm para que vea la iso como cd rom

5. Bootear desde cd, para eso presionar esc cuando bootea vmware. También se puede modificar grub de ubuntu para bootear de cd: http://www.ubuntu-es.org/?q=node/65741

6 Una vez que arranca gparted, es una buena oportunidad para redimensionar el tamaño de la partición swap (originalmente tiene 400 Mb).
https://help.ubuntu.com/community/SwapFaq

Después de estos pasos quedó todo andando sin problemas (milagro!)