<$BlogRSDUrl$>

Esos aparatos del demonio

Mis notas sobre lo que voy leyendo de ordenadores y periféricos

domingo, abril 23, 2006

Javascript, hilos y setTimeout 


Voy a contar con un poco de detalle el problema de los hilos del otro día. Aunque al final no tenía nada que ver con hilos, me ha resultado un ejercicio interesante.

Estaba programando una extensión para Mozilla Thunderbird que sacase estadísticas sobre los mensajes. Las extensiones se programan en Javascript. El código que tenía era algo así:


var MiExtension {
listaCarpetas: new Object(),

onMenuCommand: function() {
this.prepararListaCarpetas();
this.calcularEstadisticas();
this.mostrarResultados();
},

calcularEstadisticas: function() {
for (var carpeta in this.listaCarpetas)
this.calcularEstadisticasCarpeta(carpeta);
}
};


Eso lo hice el viernes, pero el sábado, como ya he contado, me levanté con ansias refactorizadoras: como a un profeta de tiempos antiguos, durante la noche en sueños un ángel me habló y me hizo ver lo que estaba mal. Estaba mezclando procesamiento y presentación. ¡Anatema! Avergonzado íntimamente de mí, hice la siguiente refactorización*:


var VentanaMiExtension {

onMenuCommand: function() {
CalculadorEstadisticas.calcular();
this.mostrarResultados(CalculadorEstadistas.obtenerResultados());
}
};

var CalculadorEstadisticas() {
listaCarpetas : new Object(),

calcular: function() {
this.preparaListaCarpetas();
this.calcularEstadisticas();
},

calcularEstadisticas: function() {
for (var carpeta in this.listaCarpetas)
this.calcularEstadisticasCarpeta(carpeta);
}

};


Es decir, dejé el código de manejo del interfaz en un objeto y el procesamiento en otro.

El problema estaba en que calcularEstadisticas() lleva mucho tiempo**. Eso tenía dos consecuencias:

1) El interfaz de Thunderbird quedaba parado mientras procesaba. Eso es muy malo porque al usuario le da la sensación de que el programa está parado.

2) Después de unos 10 segundos, Thunderbird sacaba este mensaje:


Unresponsive script

A script on this page may be busy, or it may have stopped responding. You can stop the script now, or you can continue to see if the script will complete.



Me pareció muy curioso: Para protegerse de malos desarrolladores de extensiones como yo, tenían por ahí una especie de watchdog y cuando el hilo de interfaz estaba mucho tiempo ejecutando un script, avisaba.

Para arreglar el primer problema, había decidido que la solución era poner una barra de progreso para que el usuario viese cómo avanzaba el análisis. Por lo tanto, hice algo así:


var VentanaMiExtension {

onMenuCommand: function() {
CalculadorEstadisticas.calcular();
this.mostrarResultados(CalculadorEstadistas.obtenerResultados());
},

actualizarProgreso: function(nuevoProgreso) {
// Actualizar la barra de estado y de progreso
}
};

var CalculadorEstadisticas() {
listaCarpetas : new Object(),

calcular: function() {
this.preparaListaCarpetas();
this.calcularEstadisticas();
},

calcularEstadisticas: function() {
var numCarpetas = this.listaCarpetas.length;
var carpetasProcesadas = 0;
for (var carpeta in this.listaCarpetas) {
this.calcularEstadisticasCarpeta(carpeta);
carpetasProcesadas++;
VentanaMiExtension.actualizarProgeso(carpetasProcesadas/numCarpetas);
}
}

};


El problema es que la barra de progreso no se actualizaba. Ahora me parece obvio, pero tuve que alejarme un poco del ordenador (no hay nada como tener que hacer la compra) para darme cuenta de lo que estaba pasando: el código de mi extensión se estaba ejecutando en el mismo hilo que actualizaba el interfaz de Thunderbird, así que no repintaba y daba igual que incrementase la barra de progreso.

Ahí fue cuando me puse a buscar soluciones y escribí aquello de esos hilos del demonio. No encontré una función repaint o similar, así que al final no me quedó más remedio que meter hilos.

El problema es que estábamos hablando de hilos en Javascript sobre un programa que no he hecho yo: el Thunderbird. Yo soy muy prudente (otros dirían cobarde) y eso me parecía muy arriesgado. El código para recorrer los mensajes estaba inspirado en el de otra extensión, Remove Duplicate Messages (muy útil, por cierto), y vi que tenía por ahí unos setTimeout que no entendía para qué servían. Después de mis problemas, creí llegar a entenderlo: lo utilizaba para generar un nuevo hilo.

Ahora puedo decir que sé que no es así: no hay API de hilos en Javascript. Lo que se hace con la función setTimeout es dejar el procesamiento para más tarde y así se da tiempo a hacer otras cosas. Tiene una especie de cola de scripts a ejecutar y lo que se hace es meter la función con que se llame a setTimeout en esa cola.

El caso es que, como no me quedaba más remedio porque me parecía extremadamente chapucero que apareciese el mensaje de «Unresponsive script», me puse a meter el setTimeout. El código quedó algo así:


var VentanaMiExtension {

onMenuCommand: function() {
CalculadorEstadisticas.calcular();
},

actualizarProgreso: function(nuevoProgreso) {
// Actualizar la barra de estado y de progreso
},

onEstadisticasCalculadas: function() {
this.mostrarResultados(CalculadorEstadistas.obtenerResultados());
}
};

var CalculadorEstadisticas() {
listaCarpetas : new Object(),

calcular: function() {
this.preparaListaCarpetas();
this.calcularEstadisticas(0);
}

};

function calcularEstadisticas(index) {
if (indiceCarpeta >= CalculadorEstadisticas.listaCarpetas.length) {
VentanaMiExtension.onEstadisticasCalculadas();
return;
}

var numCarpetas = CalculadorEstadisticas.listaCarpetas.length;
var carpeta = CalculadorEstadisticas.listaCarpetas[index];
CalculadorEstadisticas.calcularEstadisticasCarpeta(carpeta);
index++;
VentanaMiExtension.actualizarProgeso(index/numCarpetas);
setTimeout(calcularEstadisticas, 10, index);
}

Como se ve, ha habido muchos cambios en el código. El problema es que al meter el setTimeout se pierde el flujo de ejecución normal: las instrucciones después del setTimeout se ejecutan inmediatamente, pero la función que se llama en el setTimeout se ejecuta un rato después. Eso hace que después de llamar a calcularEstadisticas no pueda hacer nada más, porque se haría antes de que acabase de verdad de calcularlas. Por eso metí la función onEstadisticasCalculadas.

Por otra parte, de alguna manera se perdía el this al llamar a una función a través del setTimeout***, así que tuve que sacar esa función del objeto en el que estaba. El código que antes era muy bonito, con cada cosa en su objeto y un interfaz claro entre ellos, es ahora un jaleo de objetos que se llaman unos a otros.

Eso funcionó. Más o menos. El nuevo problema es que cuando tenía una carpeta con muchos mensajes (lo que hace que se tarde mucho en procesar sin haber un setTimeout) volvía a salir el odioso mensaje de «Unresponsive script». La solución: hacer el setTimeout no en el procesamiento de carpetas sino en el procesamiento de mensajes. No voy a contar los detalles, pero eso fue todavía más complicado: si hacía el setTimeout en cada mensaje, el script, en lugar de ejecutarse en 15 segundos para mi caso de prueba, se ejecutaba en 200. Así que tuve que hacerlo sólo cada cierto número de mensajes, con otras cuantas líneas de código más para liarlo todo.

Al final funciona (parece). Pero el código resultante es un jaleo mucho más difícil de entender que el código original. Por eso yo hubiese preferido una función del tipo update o PeekMessage en lugar de tener que meter hilos o la versión pobre de Javascript: el ínclito setTimeout.

Notas:

* Probablemente haya muchas cosas que mejorar en el código. Hay que tener en cuenta que en lugar de aprender Javascript y luego ponerme a programar en él, me puse a programar y cuando algo no salía, buscaba por ahí, así que no entiendo bien ni cómo funciona la herencia ni los objetos, con lo que es normal que haya burradas.

** Por las pruebas que he hecho, la mayor parte de ese tiempo ocurre en una función de librería a la que llamo y que no he hecho yo.

*** No estoy muy seguro de esto, y la verdad que me parece raro, pero creo que pasaba y ahora no tengo ganas de volver a probarlo.

Comentarios:

Gracias por tu explicacion, tuve un problema cimilar al tuyo y quedo resuelto.

saludos
Una cosa mas, tambien tuble problema con this pero lo pase como parametro al utilizar setTimeout y funciono.
Publicar un comentario

This page is powered by Blogger. Isn't yours?

Blogroll
Enlaces
Archivos

Licencia Creative Commons
Este trabajo tiene licencia Creative Commons License.