2012-09-14 30 views
9

Para un próximo proyecto con node.js necesito realizar varias tareas de limpieza a intervalos periódicos. Específicamente, algunas tareas cada milisegundo, otras cada 20 ms (50 veces por segundo) y otras cada segundo. Así que pensé en usar setInterval(), con resultados divertidos: se saltaban muchas llamadas a funciones.node.js: setInterval() omitiendo llamadas

El punto de referencia I utilizada es la siguiente:

var counter = 0; 
var seconds = 0; 
var short = 1; 
setInterval(function() { 
     counter ++; 
    }, short); 
setInterval(function() { 
     seconds ++; 
     log('Seconds: ' + seconds + ', counter: ' + 
      counter + ', missed ' + 
      (seconds * 1000/short - counter)); 
    }, 1000); 

Hay una larga temporizador de un segundo y uno corto que se puede ajustar mediante la variable short, en este caso de 1 ms. Cada segundo imprimimos la diferencia entre el número de tics esperados en el ciclo corto y el número real de veces que se actualizó el contador corto.

Aquí es cómo se comporta cuando el contador de tiempo corto es de 1 ms:

2012-09-14T23:03:32.780Z Seconds: 1, counter: 869, missed 131 
2012-09-14T23:03:33.780Z Seconds: 2, counter: 1803, missed 197 
2012-09-14T23:03:34.781Z Seconds: 3, counter: 2736, missed 264 
... 
2012-09-14T23:03:41.783Z Seconds: 10, counter: 9267, missed 733 

Muchas llamadas a funciones se omiten. Aquí está para 10 ms:

2012-09-14T23:01:56.363Z Seconds: 1, counter: 93, missed 7 
2012-09-14T23:01:57.363Z Seconds: 2, counter: 192, missed 8 
2012-09-14T23:01:58.364Z Seconds: 3, counter: 291, missed 9 
... 
2012-09-14T23:02:05.364Z Seconds: 10, counter: 986, missed 14 

Mejor, pero aproximadamente una llamada de función se salta cada segundo. Y durante 20 ms:

2012-09-14T23:07:18.713Z Seconds: 1, counter: 46, missed 4 
2012-09-14T23:07:19.713Z Seconds: 2, counter: 96, missed 4 
2012-09-14T23:07:20.712Z Seconds: 3, counter: 146, missed 4 
... 
2012-09-14T23:07:27.714Z Seconds: 10, counter: 495, missed 5 

Finalmente durante 100 ms:

2012-09-14T23:04:25.804Z Seconds: 1, counter: 9, missed 1 
2012-09-14T23:04:26.803Z Seconds: 2, counter: 19, missed 1 
2012-09-14T23:04:27.804Z Seconds: 3, counter: 29, missed 1 
... 
2012-09-14T23:04:34.805Z Seconds: 10, counter: 99, missed 1 

En este caso se salta muy pocas llamadas (la brecha aumentó a 2 después de 33 segundos y 3 después de 108 segundos

.

Los números varían, pero son sorprendentemente consistentes entre ejecuciones: la ejecución del primer punto de referencia de 1 ms tres veces produjo un retraso después de 10 segundos de 9267, 9259 y 9253.

No he encontrado referencias para este problema en particular. Está este much cited Ressig post y muchas preguntas de JavaScript relacionadas, pero la mayoría asume que el código se ejecuta en un navegador y no en node.js.

Ahora para la temida pregunta: ¿qué está pasando aquí? Solo bromeaba; obviamente, las llamadas a funciones se están omitiendo. Pero no veo el patrón. Pensé que los ciclos largos podrían estar impidiendo los cortos, pero no tiene sentido en el caso de 1 ms. Las llamadas a funciones de ciclo corto no se superponen ya que solo actualizan una variable, y el proceso node.js está cerca del 5% de CPU incluso con un ciclo corto de 1 ms. El promedio de carga es alto, alrededor de 0.50. No sé por qué miles de llamadas estresan tanto mi sistema, ya que node.js maneja many more clients perfectly; debe ser cierto que setInterval() is CPU intensive (o estoy haciendo algo mal).

Una solución obvia es agrupar llamadas de función utilizando temporizadores más largos, y luego ejecutar llamadas de función de ciclo cortas muchas veces para simular un temporizador más corto. Luego use el ciclo largo como un "carro de escoba" que hace que las llamadas perdidas en los intervalos inferiores. Un ejemplo: configure llamadas de 20 ms y 1000 ms setInterval(). Para llamadas de 1 ms: llámalas 20 veces en la devolución de llamada de 20 ms. Para la llamada de 1000 ms: compruebe cuántas veces se ha llamado a la función de 20 ms (por ejemplo, 47), haga cualquier llamada restante (por ejemplo, 3). Pero este esquema va a ser un poco complejo, ya que las llamadas pueden superponerse de maneras interesantes; tampoco será regular aunque parezca así.

La verdadera pregunta es: ¿se puede hacer mejor, ya sea con setInterval() u otros temporizadores dentro de node.js? Gracias por adelantado.

Respuesta

10

Las funciones de SetInterval en javascript no son precisas. Deberías tratar de usar un temporizador de alta resolución. Building accurate Timers in javascript

+0

¿Cómo? ¿Qué temporizadores de resolución, una biblioteca? – alexfernandez

+0

Hay muchos scripts de temporizador de alta resolución en google.http: //www.sitepoint.com/creating-accurate-timers-in-javascript/ – zer02

+0

No resolverá el problema actual. –

8

mirada a este documento: http://nodejs.org/api/timers.html#timers_settimeout_callback_delay_arg

Es importante tener en cuenta que su devolución de llamada probablemente no se llama exactamente en retrasar milisegundos - Node.js no ofrece ninguna garantía sobre el momento exacto de la devolución de llamada cuando se disparará , ni de las cosas ordenadas se dispararán. La devolución de llamada se llamará lo más cerca posible del tiempo especificado.

Esto ocurre porque el código de la aplicación bloquea el bucle de eventos. Todos los temporizadores y eventos de E/S se pueden manejar solo en el nextTick.

Puede ver este comportamiento con este código:

setInterval(function() { 
    console.log(Date.now()); 
    for (var i = 0; i < 100000000; i++) { 
    } 
}, 1); 

tratar de cambiar cuentan iteraciones y ver los resultados.

Idealmente, el temporizador se activará exactamente si la marca de las aplicaciones durará menos de un ms. Pero esto no es practicable en una aplicación real.

+0

Había leído esa referencia, pero no explica por qué. En mi punto de referencia, no tengo código de aplicación que bloquee el ciclo de eventos. La referencia a nextTick es interesante, gracias. Sin embargo, solo aleja el problema de un nivel: ¿con qué frecuencia se activa el proceso process.nextTick()? ¿Por qué y puede ser cambiado? – alexfernandez

+0

No, no quise decir 'process.nextTick()' como una solución. Quería decir que no hay forma de manejar temporizadores y eventos de E/S más a menudo que el tiempo de ejecución de una iteración de ciclo de evento. –

+0

Entendido. Pero, ¿cuánto dura una iteración de bucle de evento, en ausencia de carga? – alexfernandez

2

La respuesta es una combinación de las de Vadim y zer02, así que dejo aquí un artículo escrito. Como dijo Vadim, el sistema no puede hacer frente a las actualizaciones demasiado frecuentes, y agregar algo de carga al sistema no va a ayudar. O más bien, el tiempo de ejecución no puede hacer frente; el sistema debería ser más que capaz de disparar la devolución de llamada cada milisegundo si es necesario, pero por alguna razón inexplicable a menudo no quiere hacerlo.

La solución es usar accurate timers, como comentó zer02. No te dejes engañar por el nombre; el mecanismo utilizado es el mismo setTimeout(), pero el retardo se ajusta dependiendo del tiempo restante hasta que el temporizador se dispare. Entonces, si el tiempo se acaba, el "temporizador preciso" llamará a setTimeout (devolución de llamada, 0) que se ejecuta inmediatamente. La carga del sistema es, sorprendentemente, menor que con setInterval(): alrededor del 2% de la CPU en lugar del 5%, en mi muestra no científica.

Esta función simple puede ser útil:

/** 
* A high resolution timer. 
*/ 
function timer(delay, callback) 
{ 
    // self-reference 
    var self = this; 

    // attributes 
    var counter = 0; 
    var start = new Date().getTime(); 

    /** 
    * Delayed running of the callback. 
    */ 
    function delayed() 
    { 
     callback(delay); 
     counter ++; 
     var diff = (new Date().getTime() - start) - counter * delay; 
     setTimeout(delayed, delay - diff); 
    } 

    // start timer 
    delayed(); 
    setTimeout(delayed, delay); 
} 

de usar, basta llamar new timer(delay, callback);. (Sí, revertí el orden de los parámetros ya que tener la devolución de llamada primero es muy molesto.)

Una última nota: setTimeout (devolución de llamada, retraso) no utiliza la recursión como temía (como en: esperar un tiempo, luego invocar la devolución de llamada), simplemente coloca la devolución de llamada en una cola que será llamada por el motor de ejecución cuando llegue su turno, en el contexto global.