node.js: setInterval() skipping calls
Asked Answered
C

4

11

For an upcoming project with node.js I need to perform various housekeeping tasks at periodic times. Specifically some tasks every millisecond, others every 20 ms (50 times per second) and still others every second. So I thought about using setInterval(), with funny results: many function calls were being skipped.

The benchmark I used is as follows:

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);

There is a long timer of one second and a short one that can be adjusted using the variable short, in this case 1 ms. Every second we print the difference between the number of expected ticks in the short cycle and the actual number of times the short counter was updated.

Here is how it behaves when the short timer is 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

Many function calls are skipped. Here it is for 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

Better, but roughly one function call is skipped every second. And for 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

Finally for 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

In this case it skips very few calls (the gap increased to 2 after 33 seconds and to 3 after 108 seconds.

The numbers vary, but are surprisingly consistent between runs: Running the first 1 ms benchmark three times yielded a delay after 10 seconds of 9267, 9259 and 9253.

I have found no references for this particular problem. There is this much cited Ressig post and lots of related JavaScript questions, but most assume that the code runs in a browser and not in node.js.

Now for the dreaded question: what is going on here? Just joking; obviously function calls are being skipped. But I fail to see the pattern. I thought that the long cycles might be preventing the short ones, but it doesn't make any sense in the 1 ms case. Short cycle function calls are not overlapping since they just update a variable, and the node.js process is near 5% CPU even with a short cycle of 1 ms. Load average is high though, at about 0.50. I don't know why a thousand calls are stressing my system so much, though, since node.js handles many more clients perfectly; it must be true that setInterval() is CPU intensive (or I am doing something wrong).

An obvious solution is to group function calls using longer timers, and then run short cycle function calls many times to simulate a shorter timer. Then use the long cycle as a "broom wagon" that makes any calls missed in the lower intervals. An example: set up 20 ms and 1000 ms setInterval() calls. For 1 ms calls: call them 20 times in the 20 ms callback. For the 1000 ms call: check how many times the 20ms function has been called (e.g. 47), do any remaining calls (e.g. 3). But this scheme is going to be a bit complex, since calls may overlap in interesting ways; also it will not be regular although it may look like it.

The real question is: can it be done better, either with setInterval() or other timers within node.js? Thanks in advance.

Critical answered 14/9, 2012 at 23:39 Comment(0)
T
12

SetInterval functions in javascript are not accurate. You should try to use a high resolution timer.Building accurate Timers in javascript

Thekla answered 15/9, 2012 at 0:19 Comment(4)
How? Which resolution timers, a library?Critical
There are many high resolution timer scripts in google.sitepoint.com/creating-accurate-timers-in-javascriptThekla
Actually, it works well enough! At least for the 20 ms timer, but also (to my surprise) with the 1 ms timer. If you will be willing to update your answer and include the link I will accept it. 1 missed 156, 2 missed 156, 3 missed 157 ... 10 missed 156 and so on. For the 1 ms counter it seems to drift, albeit slowly. I will have to be careful with stack depth I suppose.Critical
No need to deal with recursion since setTimeout() does not use recursion!Critical
H
8

Look at this doc: http://nodejs.org/api/timers.html#timers_settimeout_callback_delay_arg

It is important to note that your callback will probably not be called in exactly delay milliseconds - Node.js makes no guarantees about the exact timing of when the callback will fire, nor of the ordering things will fire in. The callback will be called as close as possible to the time specified.

This happens because application code blocks the event loop. All timers and I/O events can be handled only on the nextTick.

You can see this behaviour with this code:

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

Try to change iterations count and see results.

Ideally, the timer will be triggered exactly if the applications tick will last less than one ms. But this is not practicable in a real application.

Hunchbacked answered 14/9, 2012 at 23:59 Comment(4)
I had read that reference, but it does not explain why. In my benchmark I have no application code blocking the event loop. The reference to nextTick is interesting, thanks. However it only pushes the problem one level away: how often does process.nextTick() fire? Why, and can it be changed?Critical
No, i did't mean process.nextTick() as a solution. I wanted to say that there is no way to handle timers and I/O events more often than time of execution of one event-loop iteration.Hunchbacked
Understood. But how long is one event-loop iteration, in the absence of load?Critical
It depends on the amount of blocking code and CPU speed. On my Core i5 example with setInterval without cycle does not provide the accuracy of 1 ms.Hunchbacked
C
2

The answer happens to be a combination of those given by Vadim and zer02, so I am leaving a write-up here. As Vadim said, the system cannot cope with too frequent updates, and adding some load to the system is not going to help. Or rather the runtime cannot cope; the system should be more than capable of firing the callback every millisecond if needed, but for some unexplained reason often it doesn't want to.

The solution is to use accurate timers, as zer02 commented. Do not be misled by the name; the mechanism used is the same setTimeout(), but the delay is adjusted depending on the time left until the timer should fire. So, if the time is over then the "accurate timer" will call setTimeout(callback, 0) which is run immediately. System load is, surprisingly, less than with setInterval(): about 2% of the CPU instead of 5%, in my very unscientific sample.

This simple function may come in handy:

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

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

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

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

To use, just call new timer(delay, callback);. (Yes, I reversed the order of the parameters since having the callback first is very annoying.) To stop it, set timer.running = false.

One final note: setTimeout(callback, delay) does not use recursion as I feared (as in: wait for some time, then invoke the callback), it just places the callback in a queue which will be called by the runtime when its turn comes, in the global context.

Critical answered 17/9, 2012 at 21:29 Comment(2)
Probably a silly question but how do you stop this once started? I can get it to start but can't stop it :)Hutchings
@BenClarke I have added some code to stop it, using timer.running = false.Critical
O
0

I Disabled Debugger and tried again.It worked fine for me

Outcast answered 21/12, 2020 at 14:34 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.