Skip to content

Instantly share code, notes, and snippets.

@kpdecker
Last active August 29, 2015 14:07
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save kpdecker/9583884db509a2df9041 to your computer and use it in GitHub Desktop.
Save kpdecker/9583884db509a2df9041 to your computer and use it in GitHub Desktop.
Node Periodic Timer Behaviors

Node Periodic Timer Behaviors

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

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

setTimeout

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.

Conclusion

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.

Test Case

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();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment