jueves, 5 de diciembre de 2013

Web Workers


En esta entrada veremos la implementación de un Worker de HTML5 muy sencillo pero con implicaciones de conocimiento que vale la pena aprender.

Los Worker se hicieron para ejecución de tareas asíncronas que se ejecutarán en un hilo ajeno al que está ejecutando la página web. Tan ajeno que desde el Worker no tenemos acceso al DOM de la página, es decir que toda la sincronización se hace a través de mensajes, de objetos que sean serializables. Por excelencia utilizaremos JSON.

El problema planteado es que necesitamos realizar peticiones cada 10 segundos a un endpoint el cual devolverá un valor estadísticamente único, simulando que debemos estar checando el servidor para ver si hay datos nuevos para actualizar la interfaz del usuario.

Para esto vamos a preparar nuestro entorno de desarrollo (VS2010 o VS2012):

  1. Crear un sitio web
  2. Agregar una carpeta llamada js
  3. Agregar un archivo index.htm
  4. Agregar un Generic Handler llamado GetRandom.ashx
  5. Agregar jQuery a la carpeta js
  6. Agregar dos archivos js a la carpeta: tools.js y randomWorker.js

El archivo tools.js será el que esté en comunicación con el randomWorker, éste último es el que realizará las peticiones a GetRandom.ashx para un valor nuevo.

Prepararemos nuestro archivo index.htm con el siguiente código:
<head> <title>Valor aleatorio a través de Worker</title> <script src="js/jquery-1.7.2.min.js" type="text/javascript"></script> <script src="js/tools.js" type="text/javascript"></script> </head> <body> <p>Este es el valor aleatorio generado desde el servidor: <span id="lblAleatorio"></span></p> </body>

Vamos a abrir nuestro randomWorker.js para programar la tarea asíncrona que se estará ejecutando cada 10 segundos:
self.onmessage = function (e) { var xmlhttp = new XMLHttpRequest(); xmlhttp.onreadystatechange = function () { if (xmlhttp.readyState == 4 && xmlhttp.status == 200) { postMessage(JSON.parse(xmlhttp.response)); } } var url = '../GetRandom.ashx'; xmlhttp.open('GET', url); xmlhttp.send(); }

Estamos creando una instancia de XMLHttpRequest para realizar las peticiones a GetRandom.ashx. ¿Por qué no utilizamos $.ajax?, porque jQuery está basado en el DOM y por lo tanto no puede ser utilizado desde un Worker.

Estamos asignando un manejador al evento onreadystatechange que se dispara cada vez que hay un cambio en el estado de la petición. Los valores para readyState son los siguientes:

0: Sin inicializar, es decir que el método open no ha sido llamado.
1: Cargando, ya fue abierto pero el método send no ha sido llamado.
2: El método send ya fue llamado.
3: La respuesta está siendo descargada.
4: Todas las operaciones han finalizado.

El atributo status establece un valor para el tipo de respuesta que se está recibiendo. Los códigos de estado pueden tener los siguientes valores:

1xx: El servidor ya recibió las cabeceras y se puede proceder con el envío del cuerpo de la petición.
2xx: La solicitud fue recibida por el servidor y fue correcta.
3xx: Para terminar la solicitud el cliente debe de tomar acciones adicionales. Generalmente encontraremos que se debe a que el recurso ya fue cambiado de dirección.
4xx: La sintaxis de la petición no es la correcta o bien esta no puede ser procesada. Esto indica un error del lado del cliente.
5xx: Ocurrió un error del lado del servidor.

Tomando en cuenta lo anterior, el readyState que nos interesa es el 4 y el status es el 200, es por ello que esa validación se está haciendo en el if.

Nuestro GetRandom.ashx estará devolviendo información en formato JSON por lo que procedemos a procesarla y con la función postMessage se la enviamos al cliente del Worker (tools.js)

Notemos también que no estamos utilizando la función window.setInterval para programar la petición cada 10 segundos, eso es debido a que en el Worker no tenemos acceso al DOM y por lo tanto no tenemos acceso al objeto window.

Abrimos nuestro archivo tools.js y colocamos el siguiente código:
var tools = tools || {}; tools.Worker = new Worker('js/randomWorker.js'); tools.Worker.onmessage = function (m) { $('#lblAleatorio').text(m.data.value); }; tools.Worker.onerror = function (e) { alert('Ocurrió un problema durante la ejecución del Worker'); } $(document).ready(function () { tools.Worker.postMessage('getRandomValue'); window.setInterval(function () { tools.Worker.postMessage('getRandomValue'); }, 10000); });

Estamos creando un espacio de nombres "tools" para que cualquier variable que utilicemos no entre en conflicto con otra que esté en la página. Después estamos creando una instancia del objeto Worker que será el encargado de ejecutar la tarea asíncrona establecida en el archivo randomWorker.js.

Estamos asignando un manejador al evento onmessage del objeto Worker que recibirá el resultado de la ejecución asíncrona del Worker. Notemos que el resultado viene en la propiedad data. El resultado de la ejecución la colocaremos en nuestro elemento span con el id lblAleatorio.

Por si hay algún error, estamos también estableciendo un manejador para el evento onerror. Al finalizar la carga de la página, estamos realizando una petición inicial y dejamos programada una nueva petición en intervalos de 10 segundos. El método postMessage requiere que le sea pasado un argumento, entonces estamos enviando un texto muy sencillo y que realmente en este ejercicio no tiene la mayor importancia porque de él no depende alguna ejecución por parte del Worker.

Finalmente vamos a programar nuestro GetRandom.ashx con el siguiente código:
public class GetRandom : IHttpHandler { public void ProcessRequest(HttpContext context) { context.Response.ContentType = "application/json"; context.Response.Write("{\"value\": \"" + Guid.NewGuid().ToString() + "\"}"); } public bool IsReusable { get { return false; } } }

Veamos que el ContentType de la respuesta está establecido en el MIME que necesita JSON, también deberemos notar que estamos generando un valor estadísticamente único con el objeto Guid.

Voy a visitar el archivo index.htm con IE 11. Esto es debido a que quiero hacer notar algo en particular con este navegador:

Ejecución con Internet Explorer 11

Podemos esperar más tiempo pero veremos que el valor único no es actualizado, esto se debe al caché del navegador. ¿Cómo podemos probarlo?, vamos a abrir las herramientas de desarrollo presionando F12 y activando la captura de red:

Captura del tráfico de red con la herramienta para desarrolladores de Internet Explorer 11

¿Qué está pasando?, podemos ver que el status es 304, esto significa que la petición está siendo servida desde el caché del navegador y no está yendo hasta el servidor por el nuevo valor. Para corregir este detalle vamos a ajustar el código de GetRandom.ashx de la siguiente manera:
context.Response.ContentType = "application/json"; context.Response.AppendHeader("Cache-Control", "no-cache, no-store, must-revalidate"); context.Response.AppendHeader("Pragma", "no-cache"); context.Response.AppendHeader("Expires", "0"); context.Response.Write("{\"value\": \"" + Guid.NewGuid().ToString() + "\"}");

Una vez realizado el cambio volvemos a visitar nuestro index.htm y veremos que cada 10 segundos el valor será actualizado.

Este ha sido un ejercicio muy sencillo pero aborda varias cosas que vale la pena notar:

  1. No tenemos acceso al DOM desde un Worker.
  2. No podemos utilizar jQuery en un Worker dado que jQuery está basado en el DOM.
  3. La comunicación entre el Worker y la página (en nuestro caso tools.js) debe ser a través de mensajes de texto. Es por ello que se recomienda el uso de JSON.
  4. Deshabilitamos el caché en nuestro Handler para evitar que el navegador no realice la petición al servidor al despachar el resultado desde el caché.

Espero les haya resultado de mucha utilidad, nos vemos la siguiente.

miércoles, 4 de diciembre de 2013

Variables privadas en javascript



En esta entrada quiero abordar el tema de la implementación de variables privadas en un objeto javascript. Seguramente para muchas personas resultará muy básico, pero en proyectos que he participado y en los cursos que imparto me he encontrado que no se conoce esta característica de los objetos implementados en javascript.

Comenzaremos con un objeto sumamente sencillo y plantearemos algunas interrogantes que resultarán interesantes:
function Persona(nombre, apellido, edad) { this.Nombre = nombre; this.Apellido = apellido; this.Edad = edad; }

Notemos que se está declarando un objeto Persona que tendrá tres propiedades públicas: Nombre, Apellido y Edad. Podemos perfectamente validar la información que estamos recibiendo antes de asignarla a nuestras propiedades de la siguiente manera:
function Persona(nombre, apellido, edad) { this.Nombre = nombre; this.Apellido = apellido; if (!isNaN(edad) && Number(edad) > 18) { this.Edad = edad; } else { this.Edad = null; } } var prueba = new Persona('Pepe', 'Pérez', 30); if (prueba.Edad) alert('La edad es: ' + String(prueba.Edad)); else alert('La edad no fue asignada');

Si primero lo probamos con 30 veremos que la edad es correctamente asignada, pero si le asignamos un valor menor o igual a 18 o bien una palabra, veremos que la propiedad Edad tendrá un valor null.

Hasta ahora todo va bien, pero, ¿qué pasa si una vez que es construido el objeto le asignamos un valor no válido a la propiedad pública Edad?:
var prueba = new Persona('Pepe', 'Pérez', 'Hola'); if (prueba.Edad) alert('La edad es: ' + String(prueba.Edad)); else { alert('La edad no fue asignada'); prueba.Edad = 'Hola'; alert('La edad es: ' + prueba.Edad); }

Veremos que el valor 'Hola' habrá sido asignado a Edad sin problema alguno.

Para evitar este problema y validar la información antes de que ésta sea actualizada en nuestros objetos, podemos implementar miembros privados expuestos a través de funciones públicas. En este caso lo haremos con la Edad:
function Persona(nombre, apellido, edad) { this.Nombre = nombre; this.Apellido = apellido; var m_Edad = null; if (!isNaN(edad) && Number(edad) > 18) { m_Edad = edad; } this.getEdad = function () { return m_Edad; } this.setEdad = function (e) { if (!isNaN(edad) && Number(edad) > 18) { m_Edad = edad; } } }

Veamos que ya no existe la propiedad pública Edad, ahora tenemos un miembro privado llamado m_Edad el cual es leído a través de la función getEdad y actualizado a través de la función setEdad. Dentro de la función setEdad estamos validando que sea una edad válida, una vez que pasó nuestra validación, procedemos a asignar el valor. Probemos con el código que habíamos escrito antes con unas pequeñas adecuaciones para utilizar las nuevas funciones:
var prueba = new Persona('Pepe', 'Pérez', 'Hola'); if (prueba.getEdad()) alert('La edad es: ' + String(prueba.getEdad())); else { alert('La edad no fue asignada'); prueba.setEdad('Hola'); alert('La edad es: ' + prueba.getEdad()); }

Veremos que el segundo mensaje mostrará 'La edad es: null', esto se debe a que la edad 'Hola' no fue asignada a nuestro miembro privado m_Edad debido a que no era un valor válido.

Espero te haya resultado de utilidad esta pequeña entrada, nos vemos la siguiente.

martes, 3 de diciembre de 2013

Índices y memoria



Habitualmente se piensa que el resultado positivo de un índice sólo se traduce en una lectura más rápida de datos. En esta entrada demostraré que también nos ayudan a reducir el uso de memoria al cargar menos páginas en el buffer de SQL Server.

Recordemos que SQL Server coloca en memoria los datos antes de poder despacharlos, esto se hace de esa forma para que la futura lectura de los datos sea mucho más rápida y no tenga que ir hasta los dispositivos de almacenamiento para recuperar la información.

Quiero hacer un ejercicio muy sencillo pero significativo con la base de datos AdventureWorks. El objetivo es obtener el total de ventas de los productos de color rojo. Analizaremos la cantidad de páginas que se cargan en memoria para poder despachar el resultado.

Nuestro query queda de la siguiente manera:
SELECT SUM(SOD.LineTotal) AS Total FROM Sales.SalesOrderDetail SOD INNER JOIN Production.Product P ON SOD.ProductID = P.ProductID WHERE P.Color = 'Red'

Para obtener la cantidad de páginas que está utilizando nuestra base de datos AdventureWorks en memoria, podemos conectarnos a ella y ejecutar el siguiente query:
SELECT COUNT(*) AS paginas_usadas FROM sys.dm_os_buffer_descriptors WHERE database_id = DB_ID()

Comenzaremos limpiando la memoria y verificando que no se estén usando páginas por parte de la base de datos AdventureWorks:
USE AdventureWorks GO CHECKPOINT DBCC DROPCLEANBUFFERS GO SELECT COUNT(*) AS paginas_usadas FROM sys.dm_os_buffer_descriptors WHERE database_id = DB_ID()

Vamos a ejecutar el query que obtiene el total de ventas de los productos de color rojo y verificamos de nueva cuenta la cantidad de páginas utilizadas por la base de datos AdventureWorks:
SELECT SUM(SOD.LineTotal) AS Total FROM Sales.SalesOrderDetail SOD INNER JOIN Production.Product P ON SOD.ProductID = P.ProductID WHERE P.Color = 'Red' GO SELECT COUNT(*) AS paginas_usadas FROM sys.dm_os_buffer_descriptors WHERE database_id = DB_ID()
En mi caso la cantidad de páginas usadas es de 772

Ahora vamos a crear un índice que nos ayude a optimizar el query, éste será creado sobre la columna ProductID e incluirá las columnas UnitPrice, UnitPriceDiscount y OrderQty las cuales son utilizadas para calcular LineTotal. Nuestro código queda de la siguiente manera:
CREATE INDEX ixSProductID ON Sales.SalesOrderDetail (ProductId) INCLUDE (UnitPrice, UnitPriceDiscount, OrderQty)

Volvemos a limpiar la memoria, ejecutamos nuestro query y verificamos la cantidad de páginas utilizadas por la base de datos AdventureWorks:
CHECKPOINT DBCC DROPCLEANBUFFERS GO SELECT SUM(SOD.LineTotal) AS Total FROM Sales.SalesOrderDetail SOD INNER JOIN Production.Product P ON SOD.ProductID = P.ProductID WHERE P.Color = 'Red' GO SELECT COUNT(*) AS paginas_usadas FROM sys.dm_os_buffer_descriptors WHERE database_id = DB_ID()
En mi caso la cantidad de páginas usadas es de 251

Notemos que la memoria necesaria para despachar el query se redujo en un 67.48%. ¿Qué debemos notar?, que los índices no sólo reducen el tiempo que tarda un query en ejecutarse sino también la cantidad de páginas que SQL Server necesita colocar en memoria para despachar el resultado.

Espero les haya resultado de utilidad esta entrada, nos vemos en la siguente.