Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Very fast endless loop without blocking I/O

Is there a faster alternative to window.requestAnimationFrame() for endless loops that don't block I/O?

What I'm doing in the loop isn't related to animation so I don't care when the next frame is ready, and I have read that window.requestAnimationFrame() is capped by the monitor's refresh rate or at least waits until a frame can be drawn.

I have tried the following as well:

function myLoop() {
    // stuff in loop
    setTimeout(myLoop, 4);
}

(The 4 is because that is the minimum interval in setTimeout and smaller values will still default to 4.) However, I need better resolution than this.

Is there anything with even better performance out there?

I basically need a non-blocking version of while(true).

like image 439
Joey Avatar asked Mar 02 '17 04:03

Joey


1 Answers

Two things that will run sooner than that setTimeout:

  • process.nextTick callbacks (NodeJS-specific):

    The process.nextTick() method adds the callback to the "next tick queue". Once the current turn of the event loop turn runs to completion, all callbacks currently in the next tick queue will be called.

    This is not a simple alias to setTimeout(fn, 0). It is much more efficient. It runs before any additional I/O events (including timers) fire in subsequent ticks of the event loop.

  • Promise settlement notifications

So those might be a tools for your toolbelt, doing a mix of one or both of those with setTimeout to achieve the balance you want.

Details:

As you probably know, a given JavaScript thread runs on the basis of a task queue (the spec calls it a job queue); and as you probably know, there's one main default UI thread in browsers and NodeJS runs a single thread.

But in fact, there are at least two task queues in modern implementations: The main one we all think of (where setTimeout and event handlers put their tasks), and the "microtask" queue where certain async operations are placed during the processing of a main task (or "macrotask"). Those microtasks are processed as soon as the macrotask completes, before the next macrotask in the main queue — even if that next macrotask was queued before the microtasks were.

nextTick callbacks and promise settlement notifications are both microtasks. So scheduling either schedules an async callback, but one which will happen before the next main task.

We can see that in the browser with setInterval and a promise resolution chain:

let counter = 0;

// setInterval schedules macrotasks
let timer = setInterval(() => {
  $("#ticker").text(++counter);
}, 100);

// Interrupt it
$("#hog").on("click", function() {
  let x = 300000;

  // Queue a single microtask at the start
  Promise.resolve().then(() => console.log(Date.now(), "Begin"));

  // `next` schedules a 300k microtasks (promise settlement
  // notifications), which jump ahead of the next task in the main
  // task queue; then we add one at the end to say we're done
  next().then(() => console.log(Date.now(), "End"));

  function next() {
    if (--x > 0) {
      if (x === 150000) {
        // In the middle; queue one in the middle
        Promise.resolve().then(function() {
          console.log(Date.now(), "Middle");
        });
      }
      return Promise.resolve().then(next);
    } else {
      return 0;
    }
  }
});

$("#stop").on("click", function() {
  clearInterval(timer);
});
<div id="ticker">&nbsp;</div>
<div><input id="stop" type="button" value="Stop"></div>
<div><input id="hog" type="button" value="Hog"></div>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>

When you run that and click the Hog button, note how the counter display freezes, then keeps going again. That's because of the 300,000 microtasks that get scheduled ahead of it. Also note the timestamps on the three log messages we write (they don't appear in the snippet console until a macrotask displays them, but the timestamps show us when they were logged).

So basically, you could schedule a bunch of microtasks, and periodically let those run out and run the next macrotask.


Note: I've used setInterval for the browser example in the snippet, but setInterval, specifically, may not be a good choice for a similar experiment using NodeJS, as NodeJS's setInterval is a bit different from the one in browsers and has some surprising timing characteristics.

like image 51
T.J. Crowder Avatar answered Oct 22 '22 09:10

T.J. Crowder