9

While working on a timing sensitive project, I used the code below to test the granularity of timing events available, first on my desktop machine in Firefox, then as node.js code on my Linux server. The Firefox run produced predictable results, averaging 200 fps on a 1ms timeout and indicating I had timing events with 5ms granularity.

Now I know that if I used a timeout value of 0, the Chrome V8 engine Node.js is built on would not actually delegate the timeout to an event but process it immediately. As expected, the numbers averaged 60,000 fps, clearly processing constantly at CPU capacity (and verified with top). But with a 1ms timeout the numbers were still around 3.5-4 thousand cycle()'s per second, meaning Node.js cannot possibly be respecting the 1ms timeout which would create a theoretical maximum of 1 thousand cycle()'s per second.

Playing with a range of numbers, I get:

  • 2ms: ~100 fps (true timeout, indicating 10ms granularity of timing events on Linux)
  • 1.5: same
  • 1.0001: same
  • 1.0: 3,500 - 4,500 fps
  • 0.99: 2,800 - 3,600 fps
  • 0.5: 1,100 - 2,800 fps
  • 0.0001: 1,800 - 3,300 fps
  • 0.0: ~60,000 fps

The behavior of setTimeout(func, 0) seems excusable, because the ECMAScript specification presumably makes no promise of setTimout delegating the call to an actual OS-level interrupt. But the result for anything 0 < x <= 1.0 is clearly ridiculous. I gave an explicit amount of time to delay, and the theoretical minimum time for n calls on x delay should be (n-1)*x. What the heck is V8/Node.js doing?

var timer, counter = 0, time = new Date().getTime();

function cycle() {
    counter++;
    var curT = new Date().getTime();
    if(curT - time > 1000) {
        console.log(counter+" fps");
        time += 1000;
        counter = 0;
    }
    timer = setTimeout(cycle, 1);
}

function stop() {
    clearTimeout(timer);
}

setTimeout(stop, 10000);
cycle();
HonoredMule
  • 719
  • 11
  • 21
  • What I'm guessing is that the Node.js team observed code in the wild that used `1` as a timeout value to mean 'ASAP'. I know lot of programmers (myself included) hesitate at writing `setTimeout(someFunc, 0)` because the `0` somehow 'feels wrong' here... A timeout of zero is no timeout. `1` seems the next logical value to mean ASAP in such code. So the Node.js code probably just has a check like `if (timeout > 1) {scheduleTimeout(someFunc, timeout);} else {scheduleNextTick(someFunc);}` – Stijn de Witt Oct 29 '15 at 09:32
  • Very interesting question BTW. I love this kind of research! – Stijn de Witt Oct 29 '15 at 09:34

2 Answers2

5

From the node.js api docs for setTimeout(cb, ms) (emphasis mine):

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.

I suppose that "as close as possible" means something different to the implementation team than to you.

[Edit] Incidentally, it appears that the setTimeout() function isn't mandated by any specification (although apparently part of the HTML5 draft). Moreover, there appears to be a 4-10ms de-facto minimum level of granularity, so this appears to be "just how it is".

The great thing about open source software is that you can contribute a patch to include a higher resolution per your needs!

maerics
  • 151,642
  • 46
  • 269
  • 291
  • @HonoredMule: those figures "don't make any sense" due to your expectations or due to the specifics of how they have implemented the software? – maerics Feb 15 '12 at 05:08
  • Incidentally, setInterval only runs if the interval is >= 1.0 in node.js, and appears to always use a true interrupt everywhere, showing 5ms granularity on Linux and 4ms granularity in Firefox. The "no guarantees" thing is expected when we're at the mercy of OS-level interrupt granularity, but that wouldn't generate 4,000 interrupts per second. What I'm looking for is an explanation of /what/ Node.js is doing, from an API level (i.e. 0 => immediate call, <= 1.0 => straight to Node's event queue, > 1.0 => true interrupt). The documentation says nothing. – HonoredMule Feb 15 '12 at 05:17
  • Sorry, never meant to post first comment as it was (wanted a paragraph break)--I deleted it after running out of edit time. Anyway, my expectations are hard to set without more detailed documentation...hence the question. – HonoredMule Feb 15 '12 at 05:19
  • @HonoredMule: so I think the answer is that you're asking for too much! =) The node.js guys have made it clear what they think is acceptable; the reason it doesn't behave like you want is due to some cost/benefit tradeoff of implementation. The great thing about OSS is that you can create your own patch if you want a specific feature or behavior. – maerics Feb 15 '12 at 05:24
  • 1
    I guess it would just be nice if people who wrote software actually told us what it does, so we know exactly where to pick up. But I shouldn't attack your answer...I chose a poor question title. "What is V8/Node.js doing here" (How is Node.js implementing setTimeout) was my real question--I'm no C developer, and was hoping for a documentation-level answer. :/ – HonoredMule Feb 15 '12 at 05:35
1

For completeness I would like to point out to the nodeJS implementation:

https://github.com/nodejs/node-v0.x-archive/blob/master/lib/timers.js#L214

Which is:

// Timeout values > TIMEOUT_MAX are set to 1.
var TIMEOUT_MAX = 2147483647; // 2^31-1
...
exports.setTimeout = function(callback, after) {
    var timer;

    after *= 1; // coalesce to number or NaN

    if (!(after >= 1 && after <= TIMEOUT_MAX)) {
        after = 1; // schedule on next tick, follows browser behaviour
    }

    timer = new Timeout(after);
    ...
}

Remember this statement:

IDLE TIMEOUTS

Because often many sockets will have the same idle timeout we will not use one timeout watcher per item. It is too much overhead.
Instead we'll use a single watcher for all sockets with the same timeout value and a linked list.

This technique is described in the libev manual: http://pod.tst.eu/http://cvs.schmorp.de/libev/ev.pod#Be_smart_about_timeouts

And we pass the same timeout value (1) here.

The implementation for Timer is here:
https://github.com/nodejs/node-v0.x-archive/blob/master/src/timer_wrap.cc

Stefan Rein
  • 8,084
  • 3
  • 37
  • 37
  • Looking back, I'm inclined to speculate that the difference in activity rate for numbers between 0 < x <= 1 must actually be a measure of the cost of a particular floating point multiplication. Thanks for revisiting this, it makes a lot more sense now. – HonoredMule Dec 15 '16 at 15:14