Recently had a code review where the distinction between setInterval
and setTimeout
under Node came up. Wanting to answer this question I put together some simple tests. The behavior of the two implementations, as of Node 0.10.30, was surprisingly reverse of my expecations, but there is a lot of nuance to how the event loop works in this case.
setInterval
's behavior is similar to what one would expect from a exec+setTimeout
loop. It's period is going to be it's exec time plus the interval value.
setInterval Delay: 50 Interval: 100 Wait mode: none
1 75 75
2 226 151
3 377 151
4 528 151
Interestingly nextTick
operations are considered to be part of the exec time.
setInterval Delay: 50 Interval: 100 Wait mode: nextTick
1 78 78
2 229 151
3 380 151
4 531 151
Tasks that are deferred via setImmediate
/setTimeout
on the other hand do not count against the period of the interval.
setInterval Delay: 50 Interval: 100 Wait mode: immediate
1 77 77
2 178 101
3 278 100
4 380 102
When the execution time moves to the point of causing event loop delay, the pattern above holds true. The notable thing in all cases is that the interval does not attempt to "recover" i.e. any missed iterations will not be restored and the period begins anew at the last execution.
setInterval Delay: 250 Interval: 100 Wait mode: none
1 72 72
2 422 350
3 772 350
4 1122 350
5 1222 100
6 1324 102
7 1424 100
Chained setTimeout
operations provides some very interesting behaviors. In some ways it acts more as one might expect setInterval
to operate.
When the chained call occurs, as long as it's in the same event loop, the timeout is corrected to execute on a period relative to the start of the event loop. I.e. a timeout of 100 will execute 100ms after the start of the event loop, not the call to setTimeout
setTimeout Delay: 50 Timeout: 100 Wait mode: none
1 100 100
2 201 101
3 302 101
4 403 101
Which this behavior holds true for the over capacity case:
setTimeout Delay: 250 Timeout: 100 Wait mode: none
1 100 100
2 350 250
3 600 250
4 850 250
It also holds true if the setTimeout
is executed within a nextTick
operation:
setTimeout deferred Delay: 50 Timeout: 100 Wait mode: none
1 102 102
2 203 101
3 304 101
4 405 101
But if run in a setImmediate
, the operation performs more as seen in the setInterval
behavior:
setTimeout deferred Delay: 50 Timeout: 100 Wait mode: none
1 101 101
2 252 151
3 403 151
4 553 150
As expected there is no "recovery" attempts causing multiple back to back executions.
setTimeout Delay: 250 Timeout: 100 Wait mode: none
1 101 101
2 351 250
3 601 250
4 851 250
5 952 101
6 1052 100
7 1153 101
setTimeout
also is a falling edge sort of operation, in that the first execution will not execute until the timeout has elapsed. setInterval
appears to execute after some arbitrary amount of time.
Real time operations need to take care when dealing with timeout operations and think about what behavior they desire for the optimal outcome. It also is advisable to have tests covering any scheduler logic to ensure that this behavior holds true from version to version of Node and V8.
const DELAY = 50,
INTERVAL = 100,
WAIT = 'none';
var count = 0,
appStart = Date.now(),
last = appStart;
function exec() {
var start = Date.now();
console.log(count, start - appStart, start - last);
last = start;
// NOP the last few to allow catch up behavior
if (count >= 4) {
return;
}
function spin() {
while (Date.now() - start < DELAY) {
}
}
if (WAIT === 'immediate') {
setImmediate(spin);
} else if (WAIT === 'nextTick') {
process.nextTick(spin);
} else {
spin();
}
}
function doInterval() {
count = 0;
last = appStart = Date.now();
console.log('setInterval Delay:', DELAY, 'Interval:', INTERVAL, 'Wait mode:', WAIT);
var interval = setInterval(function() {
if (count++ > 6) {
clearInterval(interval);
doTimeout();
return;
}
exec()
}, INTERVAL);
}
function doTimeout() {
count = 0;
last = appStart = Date.now();
console.log('setTimeout Delay:', DELAY, 'Timeout:', INTERVAL, 'Wait mode:', WAIT);
var interval = setTimeout(function recurse() {
if (count++ > 6) {
doTimeoutDeferred()
return;
}
exec();
setTimeout(recurse, INTERVAL);
}, INTERVAL);
}
function doTimeoutDeferred() {
count = 0;
last = appStart = Date.now();
console.log('setTimeout deferred Delay:', DELAY, 'Timeout:', INTERVAL, 'Wait mode:', WAIT);
var interval = setTimeout(function recurse() {
if (count++ > 6) {
return;
}
exec();
setImmediate(function() {
setTimeout(recurse, INTERVAL);
});
}, INTERVAL);
}
doInterval();